SpringBoot学习笔记_kuangshenshuo

1 微服务

(Microservices)—Martin Flower

1.1 微服务

  • 微服务架构(Microservice Architecture)

    • 一词在过去几年里广泛的传播,它用于描述一种设计应用程序的特别方式,作为一套独立可部署的服务。
    • 目前,这种架构方式还没有准确的定义,但是在围绕业务能力的组织、自动部署(automated deployment)、端智能(intelligence in the endpoints)、语言和数据的 分散控制,却有着某种共同的特征。
  • 微服务(Microservices)

    • 在过去几年里,我们发现越来越多的项目开始使用这种风格,以至于我们身边的同事在构建企业级应用时,把它理所当然的认为这是一种默认开发形式。
    • 然而,很不幸,微服务风格是什么,应该怎么开发,关于这样的理论描述却很难找到。
  • 微服务架构风格

    • 就像是把一个单独的应用程序开发为一套小服务,每个小服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API。
    • 这些服务围绕 业务能力 来构建,并通过完全自动化部署机制来独立部署。这些服务使用不同的编程语言书写,以及不同数据存储技术,并保持最低限度的 集中式管理
  • 在开始介绍微服务风格(microservice style)前,比较一下**整体风格(monolithic style)**是很有帮助的:一个完整应用程序(monolithic application)构建成一个单独的单元。

    • 企业级应用通常被构建成三个主要部分:客户端用户界面(由运行在客户机器上的浏览器的 HTML 页面、Javascript 组成)、数据库(由许多的表构成一个通用的、相互关联的数据管理系统)、服务端应用
    • 服务端应用处理 HTTP 请求,执行领域逻辑(domain logic),检索并更新数据库中的数据,使用适当的 HTML 视图发送给浏览器。
    • 服务端应用是完整的 ,是一个单独的的逻辑执行。任何对系统的改变都涉及到重新构建和部署一个新版本的服务端应用程序。

2 SpringBoot:快速入门

2.1 什么是Spring

Spring是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson 。

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

2.2 Spring是如何简化Java开发的

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

1、基于POJO的轻量级和最小侵入性编程;

2、通过IOC,依赖注入(DI)和面向接口实现松耦合;

3、基于切面(AOP)和惯例进行声明式编程;

4、通过切面和模版减少样式代码;

2.3 什么是SpringBoot

  • 学过javaweb的同学就知道,开发一个web应用,从最初开始接触Servlet结合Tomcat, 跑出一个Hello Wolrld程序,是要经历特别多的步骤; 后来就用了框架Struts,再后来是SpringMVC,到了现在的SpringBoot,过一两年又会有其他web框架出现;不知道你们有没经历过框架不断的演进,然后自己开发项目所有的技术也再不断的变化、改造,反正我是都经历过了,哈哈。言归正传,什么是SpringBoot呢,就是一个javaweb的开发框架,和SpringMVC类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置, you can “just run”,能迅速的开发web应用,几行代码开发一个http接口。

  • 所有的技术框架的发展似乎都遵循了一条主线规律:

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

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

  • Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。

    • 也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。
    • Spring Boot 以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 Spring Boot 应用只需要很少的 Spring 配置。
    • 同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),Spring Boot 应用中这些第三方库几乎可以零配置的开箱即用,
  • 简单来说就是SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。

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

Spring Boot的主要优点

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

2.4 微服务

什么是微服务?

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

单体应用架构

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

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

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

微服务架构

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

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

这样做的好处是:

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

Martin Flower 于 2014 年 3 月 25 日写的《Microservices》,详细的阐述了什么是微服务。

  • 原文地址:http://martinfowler.com/articles/microservices.html
  • 翻译:https://www.cnblogs.com/liuning8023/p/4493156.html

如何构建微服务

一个大型系统的微服务架构,就像一个复杂交织的神经网络,每一个神经元就是一个功能元素,它们各自完成自己的功能,然后通过http相互请求调用。比如一个电商系统,查缓存、连数据库、浏览页面、结账、支付等服务都是一个个独立的功能服务,都被微化了,它们作为一个个微服务共同构建了一个庞大的系统。如果修改其中的一个功能,只需要更新升级其中一个功能服务单元即可。

但是这种庞大的系统架构给部署和运维带来很大的难度。于是,spring为我们带来了构建大型分布式微服务的全套、全程产品:

  • 构建一个个功能独立的微服务应用单元,可以使用springboot,可以帮我们快速构建一个应用;
  • 大型分布式网络服务的调用,这部分由spring cloud来完成,实现分布式;
  • 在分布式中间,进行流式数据计算、批处理,我们有spring cloud data flow。
  • spring为我们想清楚了整个从开始构建应用到大型分布式应用全流程方案。

2.5 HelloWorld

准备工作

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

我的环境准备:

  • java version “1.8.0_181”
  • Maven-3.6.1
  • SpringBoot 2.x 最新版

开发工具:

  • IDEA

创建基础项目

Spring官方提供了非常方便的工具

Spring Initializr:https://start.spring.io/ 来帮助我们创建Spring Boot应用。

【目标一:使用Spring Initializr页面创建项目

步骤:

  1. 打开 https://start.spring.io/
  2. 填写项目信息
  3. 点击”Generate Project“按钮生成项目;下载此项目
  4. 解压项目包,并用编译器以Maven项目导入,以IntelliJ IDEA为例:
  5. 导入这个Maven项目,一路下一步即可,直到项目导入完毕。如果是第一次使用,可能速度会比较慢,需要耐心等待一切就绪

项目结构分析

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

  • 程序的主程序类
  • 一个 application.properties 配置文件
  • 一个测试类

生成的DemoApplication和测试包下的DemoApplicationTests类都可以直接运行来启动当前创建的项目,由于目前该项目未配合任何数据访问或Web模块,程序会在加载完Spring之后结束运行。

pom.xml 分析

打开pom.xml,看看Spring Boot项目的依赖:

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

<dependencies>
    <!-- web场景启动器 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- springboot单元测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <!-- 剔除依赖 -->
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

<build>
    <plugins>
        <!-- 打包插件 -->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

如上所示,主要有四个部分:

  • 项目元数据信息:创建时候输入的Project Metadata部分,也就是Maven项目的基本元素,包括:groupId、artifactId、version、name、description等
  • parent:继承spring-boot-starter-parent的依赖管理,控制版本与打包等内容
  • dependencies:项目具体依赖,这里包含了spring-boot-starter-web用于实现HTTP接口(该依赖中包含了Spring MVC),官网对它的描述是:使用Spring MVC构建Web(包括RESTful)应用程序的入门者,使用Tomcat作为默认嵌入式容器。;spring-boot-starter-test用于编写单元测试的依赖包。更多功能模块的使用我们将在后面逐步展开。
  • build:构建配置部分。默认使用了spring-boot-maven-plugin,配合spring-boot-starter-parent就可以把Spring Boot应用打包成JAR来直接运行。

编写HTTP接口

  1. 在主程序的同级目录下,新建一个controller包【一定要在同级目录下,否则识别不到
  2. 在包中新建一个Controller类
package com.kuang.springboot.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        return "Hello World";
    }
    
}
  1. 编写完毕后,从主程序启动项目,浏览器发起请求,看页面返回;

    • 控制台输出了SpringBoot 的 banner

    • 控制条输出了 Tomcat 访问的端口号!

    • #更改项目的端口号
      server.port=8081
      
    • 访问 hello 请求,字符串成功返回!
      在这里插入图片描述

  2. 将项目打成jar包
    在这里插入图片描述
    如果遇到以上错误,可以配置打包时 跳过项目运行测试用例

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

如果打包成功,则会在target目录下生成一个 jar 包
在这里插入图片描述

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

小结

  • 简单几步,就完成了一个web接口的开发,SpringBoot就是这么简单。所以我们常用它来建立我们的微服务项目!

彩蛋

如何更改启动时显示的字符拼成的字母,SpringBoot呢?

点击这个链接 ,直接输入要生成的字母,系统会自动转换,然后复制下面转换好的字符;

到项目下的 resources 目录下新建一个txt文件就可以,banner.txt

 /\/\/\                            /  \                   
| \  / |                         /      \                 
|  \/  |                       /          \               
|  /\  |----------------------|     /\     |              
| /  \ |                      |    /  \    |              
|/    \|                      |   /    \   |              
|\    /|                      |  | (  ) |  |              
| \  / |                      |  | (  ) |  |              
|  \/  |                 /\   |  |      |  |   /\         
|  /\  |                /  \  |  |      |  |  /  \        
| /  \ |               |----| |  |      |  | |----|       
|/    \|---------------|    | | /|   .  |\ | |    |       
|\    /|               |    | /  |   .  |  \ |    |       
| \  / |               |    /    |   .  |    \    |       
|  \/  |               |  /      |   .  |      \  |       
|  /\  |---------------|/        |   .  |        \|       
| /  \ |              /   NASA   |   .  |  NASA    \      
|/    \|              (          |      |           )     
|/\/\/\|               |    | |--|      |--| |    |       
------------------------/  \-----/  \/  \-----/  \--------
                        \\//     \\//\\//     \\//        
                         \/       \/  \/       \/      

然后重启试试吧!

使用IDEA快速构建项目

之前,我们在官网上直接快速构建了一个springboot项目,IDEA也可以做到,我们来看下具体步骤:

  1. 创建一个新项目

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

  3. 填写项目信息

  4. 选择初始化的组件

  5. 填写项目路径

  6. 等待项目构建成功

  7. 我们在SpringBootApplication的同路径下,建一个包 controller ,建立一个类HelloSpringBoot

  8. 编写代码

    //@RestController 的意思就是 Controller 里面的方法都以 json 格式输出
    @RestController
    public class HelloSpringBoot {
    
        @RequestMapping("/hello")
        public String hello(){
            return "Hello,SpringBoot!";
        }
    }
    
  9. 启动主程序,打开浏览器访问 http://localhost:8080/hello,就可以看到效果了!

我们在之后的学习中,直接使用IDEA创建项目即可,方便快捷!这么简单的东西背后一定有故事,我们之后去进行一波源码分析!

2.6 运行原理探究

pom.xml

我们之前写的HelloSpringBoot,到底是怎么运行的呢,我们来看pom.xml文件

其中它主要是依赖一个父项目

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

进入父项目

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

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

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

启动器

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

springboot-boot-starter:就是spring-boot的场景启动器(在什么情况下启动)

比如在web下:spring-boot-starter-web 帮我们导入了web模块正常运行所依赖的组件;

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

主程序

//@SpringBootApplication 来标注一个主程序类 , 说明这是一个Spring Boot应用
@SpringBootApplication
public class Springboot01HelloworldApplication {

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

但是一个简单的启动类并不简单!我们来分析一下这些注解都干了什么

@SpringBootApplication

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

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

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    //.....
}
@ComponentScan
  • 这个注解在Spring中很重要 , 它对应XML配置中的元素。
  • 功能就是自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中 ;
  • 意义:SpringBoot的配置类 ;标注在某个类上 , 表示这是一个SpringBoot的配置类;

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

@Configuration //点进去得到下面的 @Component
public @interface SpringBootConfiguration {
}
@Component
public @interface Configuration {
}

@Configuration:配置类上来标注这个注解,说明这是一个配置类 ,配置类—即----配置文件;

我们继续点进去,发现配置类也是容器中的一个组件。@Component 。这就说明,启动类本身也是Spring中的一个组件而已,负责启动应用!

@EnableAutoConfiguration

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n1haweuE-1627865813998)(https://blog.kuangstudy.com/usr/uploads/2019/10/3676838154.png)]

@EnableAutoConfiguration :开启自动配置功能

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

我们点击去查看发现注解@AutoConfigurationPackage : 自动配置包

@AutoConfigurationPackage //自动配置包
@Import({AutoConfigurationImportSelector.class}) //导入哪些组件的选择器
public @interface EnableAutoConfiguration {
}

再点进去看到一个 @Import({Registrar.class})

@Import({Registrar.class})
public @interface AutoConfigurationPackage {
}
@import

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

Registrar.class 将主配置类 【即@SpringBootApplication标注的类】的所在包及包下面所有子包里面的所有组件扫描到Spring容器 ;

退到上一步,继续看:@Import({AutoConfigurationImportSelector.class}) :给容器导入组件 ;

AutoConfigurationImportSelector : 自动配置导入选择器,那么它会导入哪些组件的选择器呢? ;

我们点击去这个类看源码:

  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);
            }
        }
    }
    
  4. 我们根据源头打开spring.factories的配置文件 , 看到了很多自动配置的文件;这就是自动配置根源所在!
    在这里插入图片描述

WebMvcAutoConfiguration

我们在上面的自动配置类随便找一个打开看看,比如 : WebMvcAutoConfiguration
在这里插入图片描述

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

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

自动配置原理分析.xmind

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

结论:

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

Run

我最初以为就是运行了一个main方法,没想到却 开启了一个服务

@SpringBootApplication
public class SpringbootDemo02Application {

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

SpringApplication.run分析

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

SpringApplication

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

  1. 推断应用的类型是普通的项目还是Web项目
  2. 查找并加载所有可用初始化器 , 设置到initializers属性中
  3. 找出所有的应用程序监听器,设置到listeners属性中
  4. 推断并设置main方法的定义类,找到运行的主类

查看构造器

public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
    this.sources = new LinkedHashSet();
    this.bannerMode = Mode.CONSOLE;
    this.logStartupInfo = true;
    this.addCommandLineProperties = true;
    this.addConversionService = true;
    this.headless = true;
    this.registerShutdownHook = true;
    this.additionalProfiles = new HashSet();
    this.isCustomEnvironment = false;
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = this.deduceMainApplicationClass();
}

run方法

在这里插入图片描述

3 SpringBoot:配置文件及自动配置原理

配置文件

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

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

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

YAML

YAML是 “YAML Ain’t a Markup Language” (YAML不是一种置标语言)的递归缩写。

在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种置标语言)

YAML A Markup Language :是一个标记语言

YAML isnot Markup Language :不是一个标记语言

标记语言

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

yaml配置:

server:
    prot: 8080

xml配置:

<server>
    <port>8081<port>
</server>

YAML语法

基础语法:

k:(空格) v   

以此来表示一对键值对(空格不能省略);以空格的缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的

注意 :属性和值的大小写都是十分敏感的。例子:

server:
    port: 8081
    path: /hello

值的写法

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

k: v

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

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

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

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

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

对象、Map(键值对)

k: 
    v1:
    v2:

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

student:
    name: qinjiang
    age: 3

行内写法

student: {name: qinjiang,age: 3}

数组( List、set )

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

pets:
 - cat
 - dog
 - pig

行内写法

pets: [cat,dog,pig]

修改SpringBoot的默认端口号

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

server.port=8081

注入配置文件

.yml

1. 如果要使用properties配置文件可能导入时存在乱码现象 , 需要在IDEA中进行调整 , 我们这里直接使用yml文件 , 将默认的 application.properties后缀修改为yml

2. 导入配置文件处理器

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

3.编写yml 配置文件

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

4.在SpringBoot的主程序的同级目录下建包,只有这样,主程序才会对这些类生效 ; 我们建一个pojo的包放入我们的Person类和Dog类;

@Component //注册bean
@ConfigurationProperties(prefix = "person")
public class Person {

    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;

    //get,set方法
    //toString方法
    
}
@ConfigurationProperties
  1. 作用:
  • 配置文件中配置的每一个属性的值,映射到这个组件中
  • 告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
  • 参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应
  • 只有这个组件是容器中的组件,才能使用容器提供的@ConfigurationProperties功能
prefix
  • 通过prefix 定位到以 person 开头
  • 保证 属性名字 和 application.yml 中一样,这样就能自动匹配
  • 添加 get set 方法,一个都不能少,不然就启动不成功

记得写上 toString()方法,方便调试输出结果

package com.kuang.springbootdemo03.pojo;
@Component
public class Dog {
    private String name;
    private Integer age;
    
    //get、set方法
    //toString()方法  
}

5.确认无误后,到测试单元中进行测试,看是否注入成功!

@SpringBootTest
class Springboot02ConfigApplicationTests {

    @Autowired
//    private Dog dog;
    private Person person;
    @Test
    void contextLoads() {
//        System.out.println(dog);
        System.out.println(person);
    }

运行结果
在这里插入图片描述

.properties

我们上面采用的方法都是最简单的方式,开发中最常用的;

那我们来唠唠其他的实现方式,道理都是相同得;写还是那样写;

配置文件除了yml还有我们之前常用的properties , 我们没有讲 , properties配置文件在写中文的时候,会有乱码 , 我们需要去IDEA中设置编码格式为UTF-8;
settings-->FileEncodings 中配置;

在这里插入图片描述
还有,我们的类和配置文件直接关联着 , 我们使用的是@configurationProperties的方式,

还有一种方式是使用@value
在这里插入图片描述
在这里插入图片描述

@Component //注册bean
public class Person {
    //直接使用@value
    @Value("${person.name}") //从配置文件中取值
    private String name;
    @Value("#{11*2}")  //#{SPEL} Spring表达式
    private Integer age;
    @Value("true")  // 字面量
    private Boolean happy; 
}

结果
在这里插入图片描述
这个使用起来并不友好!我们需要为每个属性单独注解赋值,比较麻烦;我们来看个功能对比图在这里插入图片描述

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

结论:

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

JSR303数据校验

  • JSR是Java Specification Requests的缩写,意思是Java 规范提案

    • 是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。
    • 任何人都可以提交JSR,以向Java平台增添新的API和服务。
    • JSR已成为Java界的一个重要标准。
  • JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,

    • Hibernate Validator 是 Bean Validation 的参考实现 .
    • Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,
    • 除此之外还有一些附加的 constraint。
 @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
 
 Boolean检查
 @AssertTrue	  验证Boolean对象是否为true
 @AssertFalse  验证Boolean对象是否为false
 
 长度检查
 @Size(min=,max=)		验证对象(Array,Collection,Map,String) 长度是否在给定的范围之内
 @Length(min=,max=)		Validates that the annotated string is between min and max included.
 
 日期检查
 @Past		验证DateCalendar 对象是否在当前时间之前
 @Future		验证DateCalendar 对象是否在当前时间之前
 @Pattern	验证 String 对象是否符合正则表达式的规则:正则表达式
 
 ......等等
 除此之外,我们还可以自定义一些数据校验规则
  • spring-boot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。我们这里来写个注解让我们的name只能支持Email格式
@Component //注册bean
@ConfigurationProperties(prefix = "person")
@Validated  //数据校验
public class Person {
    //@Value("${person.name}")
    @Email //name必须是邮箱格式
    private String name;
}

运行结果
在这里插入图片描述
使用数据校验,可以保证数据的正确性;

加载指定配置文件

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

我们去在resources目录下新建一个person.properties文件

name=kuangshen

然后在我们的代码中指定加载person.properties文件

@PropertySource(value = "classpath:person.properties")
@Component //注册bean
public class Person {

    @Value("${name}")
    private String name;

    ......  
}

配置文件占位符

随机数

${random.value}、${random.int}、
${random.long}、${random.int(10)}等等

占位符引用其他属性的值,如果不存在可以设置默认值

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

多环境切换

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

方式一:多配置文件

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

例如:application-test.properties 代表测试环境配置 application-dev.properties 代表开发环境配置

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

我们需要通过一个配置来选择需要激活的环境;

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

方式二:yml的多文档块

和properties配置文件中一样,但是使用yml去实现不需要创建多个配置文件,更加方便了,使用—分割,profiles 指定配置环境的名称

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

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

配置文件加载位置

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

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

在这里插入图片描述
优先级由高到底,高优先级的配置会覆盖低优先级的配置;

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

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

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

【扩展】指定位置加载配置文件

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

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

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

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

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

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

自动配置原理

配置文件到底能写什么?怎么写?

SpringBoot官方文档

分析自动配置原理

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


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

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

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

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

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

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

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

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

我们去配置文件里面试试前缀,看提示!
在这里插入图片描述
这就是自动装配的原理!

精髓:

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

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

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

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

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

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

@Conditional

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

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

作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;
在这里插入图片描述
那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。

我们怎么知道哪些自动配置类生效;我们可以通过启用 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;

#开启springboot的调试类
debug=true

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

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

Unconditional classes: (没有条件的类)

4 SpringBoot:Web开发

简介

使用SpringBoot的步骤:

  1. 创建一个SpringBoot应用,选择我们需要的模块,SpringBoot就会默认将我们的需要的模块自动配置好
  2. 手动在配置文件中配置部分配置项目就可以运行起来了
  3. 专注编写业务代码,不需要考虑以前那样一大堆的配置了。

要熟悉掌握开发,之前学习的自动配置的原理一定要搞明白!

比如SpringBoot到底帮我们配置了什么?我们能不能修改?我们能修改哪些配置?我们能不能扩展?

  • 向容器中自动配置组件 :xxxxAutoconfiguration
  • 自动配置类,封装配置文件的内容:xxxxProperties

静态资源映射规则

首先,我们搭建一个普通的SpringBoot项目,回顾一下HelloWorld程序!【演示】

那我们要引入我们小实验的测试资源,我们项目中有许多的静态资源,比如,css,js等文件,这个SpringBoot怎么处理呢?

如果我们是一个web应用,我们的main下会有一个webapp,我们以前都是将所有的页面导在这里面的,对吧!

但是我们现在的pom呢,打包方式是为jar的方式,那么这种方式SpringBoot能不能来给我们写页面呢?当然是可以的,但是SpringBoot对于静态资源放置的位置,是有规定的!

首页!

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext
	applicationContext) {
	
    return new WelcomePageHandlerMapping(new TemplateAvailabilityProviders
    (applicationContext), 
    applicationContext, 
    this.getWelcomePage(), 
    this.mvcProperties.getStaticPathPattern());
}

点进去继续看

        private Optional<Resource> getWelcomePage() {
            String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
            return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
        }

        private Resource getIndexHtml(String location) {
            return this.resourceLoader.getResource(location + "index.html");
        }

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

比如我访问 localhost:8080/ ,就会找静态资源文件夹下的 index.html 【测试一下】

图标

@Configuration
@ConditionalOnProperty(
    value = {"spring.mvc.favicon.enabled"},
    matchIfMissing = true
)
public static class FaviconConfiguration implements ResourceLoaderAware {
    private final ResourceProperties resourceProperties;
    private ResourceLoader resourceLoader;

    public FaviconConfiguration(ResourceProperties resourceProperties) {
        this.resourceProperties = resourceProperties;
    }

    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Bean
    public SimpleUrlHandlerMapping faviconHandlerMapping() {
        SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
        mapping.setOrder(-2147483647);
        mapping.setUrlMap(Collections.singletonMap
                          ("**/favicon.ico", this.faviconRequestHandler()));
        return mapping;
    }

    @Bean
    public ResourceHttpRequestHandler faviconRequestHandler() {
        ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
        requestHandler.setLocations(this.resolveFaviconLocations());
        return requestHandler;
    }

    private List<Resource> resolveFaviconLocations() {
        String[] staticLocations = WebMvcAutoConfiguration.
            WebMvcAutoConfigurationAdapter.
            getResourceLocations(this.resourceProperties.getStaticLocations());
        List<Resource> locations = new ArrayList(staticLocations.length + 1);
        Stream var10000 = Arrays.stream(staticLocations);
        ResourceLoader var10001 = this.resourceLoader;
        this.resourceLoader.getClass();
        var10000.map(var10001::getResource).forEach(locations::add);
        locations.add(new ClassPathResource("/"));
        return Collections.unmodifiableList(locations);
    }
}

“**/favicon.ico” , 就是我们的图标定义的格式!也是在我们的静态资源文件夹下定义,我们可以自定义来测试一下 【测试】

自己放一个图标进去,然后在配置文件中关闭SpringBoot默认的图标!

#关闭默认图标
spring.mvc.favicon.enabled=false

清除浏览器缓存!刷新网页,发现图标已经变成自己的了!

我们也可以自己通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在application.properties中配置;

spring.resources.static-locations=classpath:/hello/,classpath:/kuang/

一旦自己定义了静态文件夹的路径,原来的就都会失效了!

模板引擎

1、为什么使用html页面,不使用jsp

  • 前端交给我们的页面,是html页面。
  • 如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。
  • jsp支持非常强大的功能,包括能写Java代码,
  • 但是呢,我们现在的这种情况,SpringBoot这个项目首先是以jar的方式,不是war,像第二,我们用的还是嵌入式的Tomcat,所以呢,他现在默认是不支持jsp的
  • 那不支持jsp,如果我们直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦,那怎么办呢,SpringBoot推荐你可以来使用 模板引擎

2、模板引擎的思想

  • 那么这模板引擎,我们其实大家听到很多,其实jsp就是一个模板引擎,还有以用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但 再多的模板引擎,他们的思想都是一样的,什么样一个思想呢我们来看一下这张图。
    在这里插入图片描述
  • 模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。
    • 而这些值,从哪来呢,我们来组装一些数据,我们把这些数据找到。
    • 然后把模板和数据交给模板引擎,模板引擎按照这个数据把这表达式解析、填充到指定的位置,然后把这个数据最终生成一个我们想要的内容写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。
  • 只不过呢,就是说不同模板引擎之间,他们可能这个语法有点不一样。
  • 其他的我就不介绍了,我主要来介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,这模板引擎呢,是一个高级语言的模板引擎,他的这个语法更简单。而且呢,功能更强大。

3、Thymeleaf

1、Thymeleaf 官网

2、Thymeleaf 在Github 的主页

3、Spring官方文档

① 步骤
1 导入依赖
<!--thymeleaf模板-->
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
  • maven会自动下载jar包,我们可以去看下下载的东西;
    在这里插入图片描述
2 找一下Thymeleaf的自动配置类
  • 前面呢,我们已经引入了Thymeleaf,那这个要怎么使用呢?
    我们首先得按照SpringBoot的自动配置原理看一下我们这个Thymeleaf的自动配置规则,在按照那个规则,我们进行使用。
    我们去找一下Thymeleaf的自动配置类;
    在这里插入图片描述
  • 这个包下有我们的thymeleaf,看我们的配置类,我们选择部分
@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就可以帮我们自动渲染了。
  • 我们可以去测试一下 , 写一个Controller,跳转到一个指定页面,这个指定页面需要在 类路径下的模板目录下
  • 使用 thymeleaf 什么都不需要配置,只需要将他放在指定的文件夹下即可
3 关闭 thymeleaf 的缓存
spring.thymeleaf.cache=false

为什么关闭 thymeleaf 的缓存?

  • thymeleaf是一个模板引擎,缓存的意思是加载一次模板之后便不会在加载了,
    • 对于生产环境应该加上缓存,但是在开发过程中如果打开缓存,不方便开发人员调试。
    • 试想一下,改一行html,就需要重启服务器,肯定是不方便的。
  • 总结一下:
    本地开发环境下,需要把缓存关闭,否则调试成本太大。其他环境下缓存都需要打开。
4 controller层

查出一些数据,在页面中展示

@RequestMapping("/success")
public String success(Model model){
    //存入数据
    model.addAttribute("msg","Hello,Thymeleaf");
    //classpath:/templates/success.html
    return "success";
}
5 使用 thymeleaf 约束
//需要在html文件中导入命名空间的约束
xmlns:th="http://www.thymeleaf.org"
6 前端页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>狂神说</title>
</head>
<body>
<h1>Success</h1>

<!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样-->
<div th:text="${msg}"></div>
</body>
</html>

OK,入门搞定,我们来认真学习一下Thymeleaf的使用语法!

② th语法

使用任意的 th:attr 来替换Html中原生属性的值!
在这里插入图片描述

③ 表达式语法
1、${…}:获取变量值;
  • 获取对象的属性、调用方法

  • 使用内置的基本对象: #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.

  • 内置的一些工具对象: ${session.foo}

    • execInfo : information about the template being processed.

    • messages : methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using

    • {…} syntax.

    • 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.

    • ids : methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).

2、*{…}:选择表达式:和${}在功能上是一样;
  • 补充:配合 th:object="${session.user}:
    <div th:object="${session.user}">
        <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
        <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
        <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
    </div>
    
3、#{…}:获取国际化内容
4、@{…}:定义URL
  • @{/order/process(execId=${execId},execType=‘FAST’)}
5、~{…}:片段引用表达式
  • <div th:insert="~{commons :: main}">...</div>
    
6、字面量
  • 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 ,…

7、文本操作
  • String concatenation: +
  • Literal substitutions: |The name is ${name}|
8、数学运算
  • Binary operators: + , - , * , / , %
  • Minus sign (unary operator): -
9、布尔运算
  • Binary operators: and , or
  • Boolean negation (unary operator): ! , not
10、比较运算
  • Comparators: > , < , >= , <= ( gt , lt , ge , le )
  • Equality operators: == , != ( eq , ne )
11、条件运算(三元运算符)
  • If-then: (if) ? (then)
  • If-then-else: (if) ? (then) : (else)
  • Default: (value) ?: (defaultvalue)
12、Special tokens
  • No-Operation: _
④ 测试练习

我们编写一个Controller,放一些数据

@RequestMapping("/success2")
public String success2(Map<String,Object> map){
    //存入数据
    map.put("msg","<h1>Hello</h1>");
    map.put("users", Arrays.asList("qinjiang","kuangshen"));
    //classpath:/templates/success.html
    return "success";
}

前端

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

<div th:text="${msg}"></div>
<!--不转义-->
<div th:utext="${msg}"></div>

<!--遍历数据-->
<!--th:each每次遍历都会生成当前这个标签:官网#9-->
<h4 th:each="user :${users}" th:text="${user}"></h4>

<h4>
    <!--行内写法:官网#12-->
    <span th:each="user:${users}">[[${user}]]</span>
</h4>

</body>
</html>

很多样式,我们即使现在学习了,也会忘记,所以我们在学习过程中,需要使用什么,根据官方文档来查询,才是最重要的,要熟练使用官方文档!

SpringMVC自动配置

在进行测试前,我们还需要知道一个东西,就是SpringBoot 对我们的SpringMVC还做了哪些配置,包括如何扩展,如何定制。只有把这些都搞清楚了,我们在之后使用才会更加得心应手。 途径一:源码分析,途径二:官方文档

地址 : https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/html/boot-features-developing-web-applications.html

我们来阅读一段官方文档:

//29.1.1 Spring MVC Auto-configuration
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.

The auto-configuration adds the following features on top of Spring’s defaults:
//包含视图解析器
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
//支持静态资源文件夹的路径,以及webjars
Support for serving static resources, including support for WebJars (covered later in this document)).
//自动注册了Converter:
【转换器,这就是我们网页提交数据到后台自动封装成为对象的东西,比如把18字符串自动转换为int类型】
//Formatter:【格式化器,比如页面给我们了一个2019-8-10,它会给我们自动格式化为Date对象】

Automatic registration of Converter, GenericConverter, and Formatter beans.
//HttpMessageConverters:SpringMVC用来转换Http请求和响应的的,比如我们要把一个User对象转换为JSON字符串,可以去看官网文档解释;
Support for HttpMessageConverters (covered later in this document).
//定义错误代码生成规则的
Automatic registration of MessageCodesResolver (covered later in this document).
//首页定制
Static index.html support.
//图标定制
Custom Favicon support (covered later in this document).
//初始化数据绑定器:帮我们把请求数据绑定到JavaBean中!
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

我们来仔细对照,看一下它怎么实现的,它告诉我们SpringBoot已经帮我们自动配置好了SpringMVC,然后自动配置了哪些东西呢?

ContentNegotiatingViewResolver

自动配置了ViewResolver,就是我们之前学习的SpringMVC的视图解析器:即根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。我们去看看这里的源码:我们找到 WebMvcAutoConfiguration , 然后搜索ContentNegotiatingViewResolver。找到如下方法!

@Bean //我们在这里确实看到已经给容器中注册了一个bean
@ConditionalOnBean({ViewResolver.class})
@ConditionalOnMissingBean(name = {"viewResolver"},value = 
                          {ContentNegotiatingViewResolver.class})
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
    ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
    resolver.setContentNegotiationManager(
        (ContentNegotiationManager)beanFactory.
        getBean(ContentNegotiationManager.class));
    resolver.setOrder(-2147483648);
    return resolver;
}

我们可以点进这类看看!找到对应的解析视图的代码

注解说明:@Nullable 即参数可为null

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

    String mediaTypeInfo = this.logger.isDebugEnabled() && 
    	requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : "";
    if (this.useNotAcceptableStatusCode) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
        }
        return NOT_ACCEPTABLE_VIEW;
    } else {
        this.logger.debug("View remains unresolved" + mediaTypeInfo);
        return null;
    }
}

我们继续点进去看,他是怎么获得候选的视图的呢? getCandidateViews中看到他是把所有的视图解析器拿来,进行while循环,挨个解析!

 Iterator var5 = this.viewResolvers.iterator();

所以得出结论:ContentNegotiatingViewResolver 这个视图解析器就是用来组合所有的视图解析器的

我们再去研究下他的组合逻辑,看到有个属性viewResolvers,看看它是在哪里进行赋值的!

protected void initServletContext(ServletContext servletContext) {        
    //这里它是从beanFactory工具中获取容器中的所有视图解析器,
    //ViewRescolver.class , 把所有的视图解析器来组合的
    Collection<ViewResolver> matchingBeans = BeanFactoryUtils.
        beansOfTypeIncludingAncestors(this.obtainApplicationContext(), 
             ViewResolver.class).values();
    ViewResolver viewResolver;
    if (this.viewResolvers == null) {
        this.viewResolvers = new ArrayList(matchingBeans.size());

既然它是在容器中去找视图解析器,我们是否可以猜想,我们就可以去实现定制了呢?

我们可以自己给容器中去添加一个视图解析器;这个类就会帮我们自动的将它组合进来;我们去实现一下

我们在我们的主程序中去写一个视图解析器来试试;

@Bean //放到bean中
public ViewResolver myViewResolver(){
    return new MyViewResolver();
}

//我们写一个静态内部类,视图解析器就需要实现ViewResolver接口
private static class MyViewResolver implements ViewResolver{
    @Override
    public View resolveViewName(String s, Locale locale) throws Exception {
        return null;
    }
}

怎么看我们自己写的视图解析器有没有起作用呢?我们给dispatcherServlet中的 doDispatch方法加个断点进行调试一下,因为所有的请求都会走到这个方法中
在这里插入图片描述
我们启动我们的项目,然后随便访问一个页面,看一下Debug信息;

找到this;
在这里插入图片描述
找到视图解析器,我们看到我们自己定义的就在这里了;
在这里插入图片描述
所以说,我们如果想要使用自己定制化的东西,我们只需要给容器中添加这个组件就好了!剩下的事情SpringBoot就会帮我们做了

转换器和格式化器

找到格式化转换器

@Bean
public FormattingConversionService mvcConversionService() {            
//拿到配置文件中的格式化规则
    WebConversionService conversionService = new WebConversionService(
        this.mvcProperties.getDateFormat());
    this.addFormatters(conversionService);
    return conversionService;
}

点击去

public String getDateFormat() {
    return this.dateFormat;
}

可以看到在我们的Properties文件中,我们可以进行自动配置它!如果注册了自己的格式化方式,就会注册到Bean中,否则不会注册

我们可以在配置文件中配置日期格式化的规则:
在这里插入图片描述

修改SpringBoot的默认配置

方式一

  • 这么多的自动配置,原理都是一样的,通过这个WebMVC的自动配置原理分析,我们要学会一种学习方式,通过源码探究,得出结论;这个结论一定是属于自己的,而且一通百通。

  • SpringBoot的底层,大量用到了这些设计细节思想,所以,没事需要多阅读源码!得出结论;

  • SpringBoot在自动配置很多组件的时候,

    • 先看容器中有没有用户自己配置的(如果用户自己配置@bean)
    • 如果有就用用户配置的,如果没有就用自动配置的;
    • 如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来!

扩展使用SpringMVC

  • If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

  • 如果您希望保留Spring Boot MVC功能,并且希望添加其他MVC配置**(拦截器、格式化程序、视图控制器和其他功能),则可以添加自己的@configuration类,类型为webmvcconfiguer,但不添加@EnableWebMvc。如果希望提供RequestMappingHandlerMapping、RequestMappingHandlerAdapter或ExceptionHandlerExceptionResolver的自定义实例,则可以声明WebMVCregistrationAdapter**实例来提供此类组件。

  • 我们要做的就是编写一个@Configuration注解类,并且类型要为WebMvcConfigurer,还不能标注@EnableWebMvc注解;我们去自己写一个;

我们新建一个包叫config,写一个类MyMvcConfig;

package com.kuang.myproject.config;

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

//应为类型要求为WebMvcConfigurer,所以我们实现其接口
//可以使用自定义类扩展MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //浏览器发送/kuang , 就会跳转到success页面;
        registry.addViewController("/kuang").setViewName("success");
    }
}

我们去浏览器访问一下:
在这里插入图片描述
确实也跳转过来了!所以说,我们要扩展SpringMVC,官方就推荐我们这么去使用,既保SpringBoot留所有的自动配置,也能用我们扩展的配置!

我们可以去分析一下原理:

  1. WebMvcAutoConfiguration 是 SpringMVC的自动配置类,里面有一个类WebMvcAutoConfigurationAdapter

  2. 这个类上有一个注解,在做其他自动配置时会导入:@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})

  3. 我们点进EnableWebMvcConfiguration这个类看一下,它继承了一个父类:DelegatingWebMvcConfiguration,这个父类中有这样一段代码

  4.  private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
          //从容器中获取所有的webmvcConfigurer
        @Autowired(required = false)
        public void setConfigurers(List<WebMvcConfigurer> configurers) {
            if (!CollectionUtils.isEmpty(configurers)) {
                this.configurers.addWebMvcConfigurers(configurers);
            }
        }
    

    我们可以在这个类中去寻找一个我们刚才设置的viewController当做参考,发现它调用了一个

this.configurers.addViewControllers(registry);

我们点进去看一下

 public void addViewControllers(ViewControllerRegistry registry) {
        Iterator var2 = this.delegates.iterator();

        while(var2.hasNext()) {            //将所有的WebMvcConfigurer相关配置来一起调用!包括我们自己配置的和Spring给我们配置的
         WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
            delegate.addViewControllers(registry);
        }
    }
  1. 所以得出结论:所有的WebMvcConfiguration都会被作用,不止Spring自己的配置类,我们自己的配置类当然也会被调用;

全面接管SpringMVC

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

全面接管即:SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己去配置!只需在我们的配置类中要加一个@EnableWebMvc.

我们看下如果我们全面接管了SpringMVC了,我们之前SpringBoot给我们配置的静态资源映射一定会无效,我们可以去测试一下;

不加注解之前,访问首页
在这里插入图片描述
给配置类加上注解:@EnableWebMvc
在这里插入图片描述
我们发现所有的SpringMVC自动配置都失效了!回归到了最初的样子;

当然,我们开发中,不推荐使用全面接管SpringMVC

思考问题?为什么加了一个注解,自动配置就失效了!我们看下源码:

  1. 这里发现它是导入了一个类,我们可以继续进去看
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}
  1. 它继承了一个父类
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
}

3.我们来回顾一下Webmvc自动配置类

@Configuration
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})//这个注解的意思就是:容器中没有这个组件的时候,这个自动配置类才生效
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
}
  1. 总结一句话:@EnableWebMvc将WebMvcConfigurationSupport组件导入进来了;而导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能!

在SpringBoot中会有非常多的xxxx Configurer帮助我们进行扩展配置,只要看见了这个,我们就应该多留心注意

RestfulCRUD

准备工作

我们现在可以来导入我们的所有提供的资源!

pojo 及 dao 放到项目对应的路径下:

pojo实体类
Department.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
    private Integer id;
    private String departmentName;
}

Employee.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
    private Integer id;
    private String lastName;
    private String email;
    private Integer gender;//0:女  1:男
    private Department department;
    private Date birth;
}

dao层

img DepartmentDao

@Repository
public class DepartmentDao {
    //模拟数据库中的数据
    private static Map<Integer, Department> departments = null;
    static {
        departments = new HashMap<Integer, Department>();//创建一个部门表
        departments.put(101, new Department(101,"教学部"));
        departments.put(102, new Department(102,"市场部"));
        departments.put(103, new Department(103,"教研部"));
        departments.put(104, new Department(104,"运营部"));
        departments.put(105, new Department(105,"后勤部"));
    }
    //获得所有部门信息
    public Collection<Department> getDepartments(){
        return departments.values();
    }
    //通过id得到部门
    public Department getDepartmentById(Integer id){
        return departments.get(id);
    }
}

EmployeeDao

@Repository
public class EmployeeDao {
    //模拟数据库的数据
    private static Map<Integer, Employee> employees = null;
    //员工所属的部门
    @Autowired
    private DepartmentDao departmentDao;
    static {
        employees = new HashMap<Integer, Employee>();//创建一个部门表
        employees.put(1001,new Employee(1001,"AA","1@qq.com",0, new Department(101,"教学部")));
        employees.put(1002,new Employee(1002,"BB","2@qq.com",1, new Department(102,"市场部")));
        employees.put(1003,new Employee(1003,"CC","3@qq.com",0, new Department(103,"教研部")));
        employees.put(1004,new Employee(1004,"DD","4@qq.com",1, new Department(104,"运营部")));
        employees.put(1005,new Employee(1005,"EE","5@qq.com",0, new Department(105,"后勤部")));
    }
    //主键自增
    private static Integer initId = 1006;
    //增加一个员工
    public void save(Employee employee){
        if (employee.getId() == null){
            employee.setId(initId++);
        }
        employee.setDepartment(departmentDao.getDepartmentById(employee.getId()));
        employees.put(employee.getId(), employee);
    }
    //查询全部员工
    public Collection<Employee> getAll(){
        return employees.values();
    }
    //通过id查询员工
    public Employee getEmployeeById(Integer id){
        return employees.get(id);
    }
    //通过id删除员工
    public void delete(Integer id){
        employees.remove(id);
    }
}

导入完毕这些之后,我们还需要导入我们的前端页面,及静态资源文件!

  • css,js等放在static文件夹下
  • html放在templates文件夹下

准备工作:OK!!!

首页实现

要求一:默认访问首页

方式一:写一个controller实现!

//会解析到templates目录下的index.html页面
@RequestMapping({"/","/index.html"})
public String index(){
    return "index";
}

方式二:自己编写MVC的扩展配置

package com.kuang.myproject.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 MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }
}
解决一个资源导入的问题;

我们将我们项目的 启动名改掉

server.servlet.context-path=/kuang

现在你访问localhost:8080 就不行了,需要访问localhost:8080/kuang

为了保证资源导入稳定,我们建议在所有资源导入时候使用 th:去替换原有的资源路径

相应的html文件头部要引入 thymeleaf 的命名空间

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<link href="asserts/css/bootstrap.min.css" 
th:href="@{/css/bootstrap.min.css}" rel="stylesheet">

比如以上这样,无论我们项目名称如何变化,它都可以自动的寻找到!看源代码就可以看出来区别:例如
在这里插入图片描述

页面国际化

第一步 :编写国际化配置文件,抽取页面需要显示的国际化页面消息。我们可以去登录页面查看一下
先在IDEA中统一设置properties的编码问题!
在这里插入图片描述
我们在resources资源文件下新建一个i18n目录,建立一个login.properties文件,还有一个login_zh_CN.properties,发现IDEA自动识别了我们要做国际化操作;文件夹变了
在这里插入图片描述
我们可以在这上面去新建一个文件;
在这里插入图片描述
弹出如下页面:我们再添加一个英文的;
在这里插入图片描述
这样就快捷多了!
在这里插入图片描述
接下来,我们就来编写配置,我们可以看到idea下面有另外一个视图;
在这里插入图片描述
这个视图我们点击 + 号就可以直接添加属性了;我们新建一个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=用户名

OK,搞定!

第二步 :我们去看一下SpringBoot对国际化的自动配置!

这里又涉及到一个类: MessageSourceAutoConfiguration ,里面有一个方法,这里发现SpringBoot已经自动配置好了管理我们国际化资源文件的组件 ResourceBundleMessageSource;

public class MessageSourceAutoConfiguration {
    private static final Resource[] NO_RESOURCES = new Resource[0];

    public MessageSourceAutoConfiguration() {
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.messages") 
    //我们的配置文件可以直接放在类路径下叫: messages.properties, 就可以进行国际化操作了
    public MessageSourceProperties messageSourceProperties() {
        return new MessageSourceProperties();
    }

    @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目录下,所以我们要去配置这个messages的路径;

 spring.messages.basename=i18n.login

第三步 : 去页面获取国际化的值;

查看Thymeleaf的文档,找到message取值操作为: #{…}。

我们去页面测试下;
在这里插入图片描述
其余同理!IDEA还有提示,非常智能的!
在这里插入图片描述
我们可以去打开项目,访问一下,发现已经自动识别为中文的了!
在这里插入图片描述

但是我们想要更好!可以根据按钮自动切换中文英文!

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

我们去我们webmvc自动配置文件,寻找一下!看到SpringBoot默认配置了

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(
    prefix = "spring.mvc",
    name = {"locale"}
)
public LocaleResolver localeResolver() {            
//容器中没有就自己配,有的话就用用户配置的
    if (this.mvcProperties.getLocaleResolver() == org.springframework.boot.
    autoconfigure.web.servlet.WebMvcProperties.LocaleResolver.FIXED) {
        return new FixedLocaleResolver(this.mvcProperties.getLocale());
    } else {
        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(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>

我们去写一个处理的组件类

//可以在链接上携带区域信息
public class MyLocaleResolver implements LocaleResolver {

    //解析请求
    @Override
    public Locale resolveLocale(HttpServletRequest request) {

        String language = request.getParameter("l");
        Locale locale = Locale.getDefault(); //如果没有获取到就使用系统默认的
        //如果请求链接不为空
        if (!StringUtils.isEmpty(language)){
            //分割请求参数
            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();
}

我们重启项目,来访问一下,发现点击按钮可以实现成功切换!

登录 + 拦截器

我们这里就先不连接数据库了,输入任意用户名都可以登录成功!

声明一个之前没有提到的问题:

templates下的页面只能通过Controller跳转实现,而static下的页面是能直接被外界访问的,就能正常访问了。

我们把登录页面的表单提交地址写一个controller!
在这里插入图片描述

<form class="form-signin" th:action="@{/user/login}">
			<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>

			<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

			<label class="sr-only" th:text="#{login.username}">Username</label>
			<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
			<label class="sr-only" th:text="#{login.password}">Password</label>
			<input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">
			<div class="checkbox mb-3">

去编写对应的controller

@Controller
public class LoginController {
//    @RequestMapping(value = "/user/login",method = RequestMethod.POST)
    @RequestMapping("/user/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Model model){

        if (!StringUtils.isEmpty(username) && "123456".equals(password)){
            //登录成功!
//            return "dashboard";
            return "redirect:/main.html";
        }else {
            //登录失败!存放错误信息
            model.addAttribute("msg","用户名密码错误");
            return "index";
        }
    }
    //先测试一下通不通
//    @RequestMapping("/user/login")
//    @ResponseBody
//    public String login(){
//        return "ok";
//    }
}

页面存在缓存,所以我们需要禁用模板引擎的缓存

#禁用模板缓存
spring.thymeleaf.cache=false

模板引擎修改后,想要实时生效!页面修改完毕后,IDEA小技巧 : Ctrl + F9 重新编译!

OK ,测试登录成功!

如果模板出现500错误,参考处理连接:https://blog.csdn.net/fengzyf/article/details/83341479
在这里插入图片描述

登录成功,发现了静态资源导入问题!

登录失败的话,我们需要将后台信息输出到前台,可以在首页标题下面加上判断!

<!--判断是否显示,使用if, ${}可以使用工具类,可以看thymeleaf的中文文档-->
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

刷新测试 :
在这里插入图片描述

优化,登录成功后,由于是转发,链接不变,我们可以重定向到首页!

我们再添加一个视图控制映射,在我们的自己的MyMvcConfig

registry.addViewController("/main.html").setViewName("dashboard");

将 Controller 的代码改为重定向;

//登录成功!防止表单重复提交,我们重定向
return "redirect:/main.html";

重定向成功之后!我们解决了之前资源没有加载进来的问题!后台主页正常显示!

但是又发现新的问题,我们可以直接登录到后台主页,不用登录也可以实现!

怎么处理这个问题呢?我们可以使用拦截器机制,实现登录检查!

我们先自定义一个拦截器

package com.kuang.myproject.component;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginHandlerInterceptor implements HandlerInterceptor {
    //目标方法执行之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object user = request.getSession().getAttribute("loginUser");
        if (user == null){//未登录,返回登录页面
            request.setAttribute("msg","没有权限,请先登录");
            request.getRequestDispatcher("/index.html").forward(request,response);
            return false;
        }else {
            //登录,放行
            return true;
        }
    }
}

然后将拦截器注册到我们的SpringMVC配置类当中!

//注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
    //注册拦截器,及拦截请求和要剔除哪些请求!
    //我们还需要过滤静态资源文件,否则样式显示不出来
    registry.addInterceptor(new LoginHandlerInterceptor())
        .addPathPatterns("/**").excludePathPatterns("/index.html","/","/user/login","/asserts/**");
}

我们然后在后台主页,获取用户登录的信息

<!--后台主页显示登录用户的信息-->
[[${session.loginUser}]]

然后我们登录测试!完美!

员工列表功能

要求 : 我们需要使用 Restful风格实现我们的crud操作!
在这里插入图片描述
看看一些具体的要求,就是我们小实验的架构;
在这里插入图片描述
我们根据这些要求,来完成第一个功能,就是我们的员工列表功能!

我们在主页点击Customers,就显示列表页面;我们去修改下

1.将首页的侧边栏Customers改为员工管理

2.a链接添加请求

<a class="nav-link" href="#" th:href="@{/emps}">员工管理</a>

3.将list放在emp文件夹下
在这里插入图片描述
4.编写处理请求的controller

@Controller
public class EmployeeController {

    @Autowired
    EmployeeDao employeeDao;

    //查询所有员工,返回列表页面
    @RequestMapping("/emps")
    public String list(Model model){
        Collection<Employee> employees = employeeDao.getAll();
        //将结果放在请求中
        model.addAttribute("emps",employees);
        return "emp/list";
    }

}

我们启动项目,测试一下看是否能够跳转,测试OK!我们只需要将数据渲染进去即可!

但是发现一个问题,侧边栏和顶部都相同,我们是不是应该将它抽取出来呢?

Thymeleaf 公共页面元素抽取

1.抽取公共片段 th:fragment 定义模板名

2.引入公共片段 th:insert 插入模板名

我们来抽取一下,使用list列表做演示!我们要抽取头部,nav标签

我们在dashboard中将nav部分定义一个模板名;

<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar" >
    <!--后台主页显示登录用户的信息-->
    <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>
    <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
    <ul class="navbar-nav px-3">
        <li class="nav-item text-nowrap">
            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a>
        </li>
    </ul>
</nav>

然后我们在list页面中去引入,可以删掉原来的nav

        <!--引入抽取的topbar-->
        <!--模板名 : 会使用thymeleaf的前后缀配置规则进行解析
        使用~{模板::标签名}-->
        <div th:insert="~{dashboard::topbar}"></div>

效果:可以看到已经成功加载过来了!
在这里插入图片描述

除了使用insert插入,还可以使用replace替换,或者include包含,三种方式会有一些小区别,可以见名知义;

我们使用replace替换,可以解决div多余的问题,可以查看thymeleaf的文档学习

侧边栏也是同理,当做练手,可以也同步一下!

保证这一步做完!

我们发现一个小问题,侧边栏激活的问题,它总是激活第一个;按理来说,这应该是动态的才对!

为了重用更清晰,我们建立一个commons文件夹,专门存放公共页面;
在这里插入图片描述

我们去页面中引入一下

<div th:replace="~{commons/bar::topbar}"></div>
<div th:replace="~{commons/bar::sitebar}"></div>

我们先测试一下,保证所有的页面没有出问题!ok!

我们来解决我们侧边栏激活问题!

1.将首页的超链接地址改到项目中

2.我们在a标签中加一个判断,使用class改变标签的值;

 <a class="nav-link active" th:class="${activeUrl=='main.html'?'nav-link active':'nav-link'}" href="#" th:href="@{/main.html}">其余同理,判断请求携带的参数,进行激活测试

3.修改请求链接

<div th:replace="~{commons/bar::sitebar(activeUrl='main.html')}"></div>
<div th:replace="~{commons/bar::sitebar(activeUrl='emps')}"></div>其余要用都是同理。

我们刷新页面,去测试一下,OK,动态激活搞定!

现在我们来遍历我们的员工信息!顺便美化一些页面,增加添加,修改,删除的按钮

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <!--添加员工按钮-->
    <h2> <button class="btn btn-sm btn-success">添加员工</button></h2>
    <div class="table-responsive">
        <table class="table table-striped table-sm">
            <thead>
                <tr>
                    <th>id</th>
                    <th>lastName</th>
                    <th>email</th>
                    <th>gender</th>
                    <th>department</th>
                    <th>birth</th>
                    <!--我们还可以在显示的时候带一些操作按钮-->
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="emp:${emps}">
                    <td th:text="${emp.id}"></td>
                    <td>[[${emp.lastName}]]</td>
                    <td th:text="${emp.email}"></td>
                    <td th:text="${emp.gender==0?'':''}"></td>
                    <td th:text="${emp.department.departmentName}"></td>
                    <!--<td th:text="${emp.birth}"></td>-->
                    <!--使用时间格式化工具-->
                    <td th:text="${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}"></td>

                    <!--操作-->
                    <td>
                        <button class="btn btn-sm btn-primary">编辑</button>
                        <button class="btn btn-sm btn-danger">删除</button>
                    </td>
                </tr>
                </tr>
            </tbody>
        </table>
    </div>
</main>

OK,显示全部员工OK!

添加员工信息

  1. 将添加员工信息改为超链接
<!--添加员工按钮-->
<h2> <a class="btn btn-sm btn-success" href="emp" th:href="@{/emp}">添加员工</a></h2>
  1. 编写对应的controller
    //to员工添加页面
    @GetMapping("/emp")
    public String toAddPage(){
        return "emp/add";
    }
  1. 添加前端页面;复制list页面,修改即可

bootstrap官网文档 : https://v4.bootcss.com/docs/4.0/components/forms/ , 我们去可以里面找自己喜欢的样式!

我这里给大家提供了编辑好的

<form>
    <div class="form-group">
        <label>LastName</label>
        <input type="text" class="form-control" placeholder="kuangshen">
    </div>
    <div class="form-group">
        <label>Email</label>
        <input type="email" class="form-control" placeholder="24736743@qq.com">
    </div>
    <div class="form-group">
        <label>Gender</label><br/>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="gender"  value="1">
            <label class="form-check-label"></label>
        </div>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="gender"  value="0">
            <label class="form-check-label"></label>
        </div>
    </div>
    <div class="form-group">
        <label>department</label>
        <select class="form-control">
            <option>1</option>
            <option>2</option>
            <option>3</option>
            <option>4</option>
            <option>5</option>
        </select>
    </div>
    <div class="form-group">
        <label>Birth</label>
        <input type="text" class="form-control" placeholder="kuangstudy">
    </div>
    <button type="submit" class="btn btn-primary">添加</button>
</form>

4.部门信息下拉框应该选择的是我们提供的数据,所以我们要修改一下前端和后端

controller

//to员工添加页面
@GetMapping("/emp")
public String toAddPage(Model model){
    //查出所有的部门,提供选择
    Collection<Department> departments = departmentDao.getDepartments();
    model.addAttribute("departments",departments);
    return "emp/add";
}

前端

<div class="form-group">
    <label>department</label>
    <!--提交的是部门的ID-->
    <select class="form-control">
        <option th:each="dept:${departments}" th:text="${dept.departmentName}" th:value="${dept.id}">1</option>
    </select>
</div>

OK,修改了controller,重启项目测试!

我们来具体实现添加功能;

1.修改add页面form表单提交地址和方式

<form th:action="@{/emp}" method="post">

2.编写controller;

    //员工添加功能,使用post接收
    @PostMapping("/emp")
    public String addEmp(){

        //回到员工列表页面,可以使用redirect或者forward,就不会被视图解析器解析
        return "redirect:/emps";
    }

回忆:重定向和转发 以及 /的问题?

原理探究 : ThymeleafViewResolver

public static final String REDIRECT_URL_PREFIX = "redirect:";
public static final String FORWARD_URL_PREFIX = "forward:";

protected View createView(String viewName, Locale locale) throws Exception {
    if (!this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
        vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
        return null;
    } else {
        String forwardUrl;
        if (viewName.startsWith("redirect:")) {
            vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
            forwardUrl = viewName.substring("redirect:".length(), viewName.length());
            RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible());
            return (View)this.getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
        } else if (viewName.startsWith("forward:")) {
            vrlogger.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName);
            forwardUrl = viewName.substring("forward:".length(), viewName.length());
            return new InternalResourceView(forwardUrl);
        } else if (this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
            vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
            return null;
        } else {
            vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a {} instance will be created for it", viewName, this.getViewClass().getSimpleName());
            return this.loadView(viewName, locale);
        }
    }
}

OK,看完源码,我们继续编写代码!

我们要接收前端传过来的属性,将它封装成为对象!首先需要将前端页面空间的name属性编写完毕!【操作】

编写controller接收调试打印【操作】

    //员工添加功能
    //接收前端传递的参数,自动封装成为对象[要求前端传递的参数名,和属性名一致]
    @PostMapping("/emp")
    public String addEmp(Employee employee){
        System.out.println(employee);
        employeeDao.save(employee); //保存员工信息
        //回到员工列表页面,可以使用redirect或者forward
        return "redirect:/emps";
    }

前端填写数据,注意时间问题
在这里插入图片描述

点击提交,后台输出正常!页面跳转及数据显示正常!OK!

那我们将时间换一个格式提交
在这里插入图片描述

提交发现页面出现了400错误!
在这里插入图片描述

生日我们提交的是一个日期 , 我们第一次使用的 / 正常提交成功了,后面使用 - 就错误了,所以这里面应该存在一个日期格式化的问题;

SpringMVC会将页面提交的值转换为指定的类型,默认日期是按照 / 的方式提交 ; 比如将2019/01/01 转换为一个date对象。

那思考一个问题?我们能不能修改这个默认的格式呢?

我们去看webmvc的自动配置文件;找到一个日期格式化的方法,我们可以看一下

@Bean
public FormattingConversionService mvcConversionService() {
    WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
    this.addFormatters(conversionService);
    return conversionService;
}

调用了 getDateFormat 方法;

    public String getDateFormat() {
        return this.dateFormat;
    }

这个在配置类中,所以我们可以自定义的去修改这个时间格式化问题,我们在我们的配置文件中修改一下;

spring.mvc.date-format=yyyy-MM-dd

这样的话,我们现在就支持 - 的格式了,但是又不支持 / 了 , 2333吧

测试OK!

员工修改功能

我们要实现员工修改功能,需要实现两步;

\1. 点击修改按钮,去到编辑页面,我们可以直接使用添加员工的页面实现

2.显示原数据,修改完毕后跳回列表页面!

我们去实现一下:首先修改跳转链接的位置;

<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.id}">编辑</a>

编写对应的controller

//to员工修改页面
@GetMapping("/emp/{id}")
public String toUpdateEmp(@PathVariable("id") Integer id,Model model){
    //根据id查出来员工
    Employee employee = employeeDao.get(id);
    //将员工信息返回页面
    model.addAttribute("emp",employee);
    //查出所有的部门,提供修改选择
    Collection<Department> departments = departmentDao.getDepartments();
    model.addAttribute("departments",departments);

    return "emp/update";
}

我们需要在这里将add页面复制一份,改为update页面;需要修改页面,将我们后台查询数据回显

<form th:action="@{/emp}" method="post">
    <div class="form-group">
        <label>LastName</label>
        <input name="lastName" type="text" class="form-control" 
        th:value="${emp.lastName}">
    </div>
    <div class="form-group">
        <label>Email</label>
        <input name="email" type="email" class="form-control" th:value="${emp.email}">
    </div>
    <div class="form-group">
        <label>Gender</label><br/>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="gender" value="1"
                   th:checked="${emp.gender==1}">
            <label class="form-check-label"></label>
        </div>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="gender" value="0"
                   th:checked="${emp.gender==0}">
            <label class="form-check-label"></label>
        </div>
    </div>
    <div class="form-group">
        <label>department</label>
        <!--提交的是部门的ID-->
        <select class="form-control" name="department.id">
            <option th:selected="${dept.id == emp.department.id}" 
            th:each="dept:${departments}"
                    th:text="${dept.departmentName}" th:value="${dept.id}">1
            </option>
        </select>
    </div>
    <div class="form-group">
        <label>Birth</label>
        <input name="birth" type="text" class="form-control" th:value="${emp.birth}">
    </div>
    <button type="submit" class="btn btn-primary">修改</button>
</form>

测试OK!

发现我们的日期显示不完美,可以使用日期工具,进行日期的格式化!

<input name="birth" type="text" class="form-control" th:value="${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}">

数据回显OK,我们继续完成数据修改问题!

修改表单提交的地址:

<form th:action="@{/updateEmp}" method="post">

编写对应的controller

@PostMapping("/updateEmp")
public String updateEmp(Employee employee){
    employeeDao.save(employee);
    //回到员工列表页面
    return "redirect:/emps";
}

发现页面提交的没有id;我们在前端加一个隐藏域,提交id;

 <input name="id" type="hidden" class="form-control" th:value="${emp.id}">

重启,修改信息测试OK!

删除员工

list页面,编写提交地址

<a class="btn btn-sm btn-danger" th:href="@{/delEmp/}+${emp.id}">删除</a>

编写Controller

    @GetMapping("/delEmp/{id}")
    public String delEmp(@PathVariable("id") Integer id){
        employeeDao.delete(id);
        return "redirect:/emps";
    }

测试OK!

定制错误页面

我们只需要在模板目录下添加一个error文件夹,文件夹中存放我们相应的错误页面,比如404.html 或者 4xx.html 等等,SpringBoot就会帮我们自动使用了!
在这里插入图片描述

注销功能

<a class="nav-link" href="#" th:href="@{/user/loginOut}">Sign out</a>

对应的controller

    @GetMapping("/user/loginOut")
    public String loginOut(HttpSession session){
        session.invalidate();
        return "redirect:/index.html";
    }

学到这里,SpringBoot的基本开发就以及没有问题了,我们后面去学习一下SpringBoot如何操作数据库以及配置Mybatis;

5 SpringBoot:Mybatis + Druid 数据访问

简介

对于数据访问层,无论是 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.1.7.RELEASE/reference/htmlsingle/#using-boot-starter

JDBC

数据源的底层就是jdbc

我去新建一个项目测试:springboot_demo_data ; 引入相应的模块!基础模块
在这里插入图片描述
项目建好之后,发现自动帮我们导入了如下的启动器:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

那我们到底应该怎么写才可以实现数据库的访问呢?

  1. 我们先连接上数据库 , 直接使用IDEA连接即可【操作】

  2. SpringBoot中,我们只需要简单的配置就可以实现数据库的连接了;

我们使用yml的配置文件进行操作!

spring:
  datasource:
    username: root
    password: 123456
    #?serverTimezone=UTC解决时区的报错
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.jdbc.Driver
  1. 配置完这一些东西后,我们就可以直接去使用了,因为SpringBoot已经默认帮我们进行了自动配置;我们去测试类测试一下
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootDemoDataApplicationTests {

    //DI注入数据源
    @Autowired
    DataSource dataSource;

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

输出结果:我们可以看到他默认给我们配置的数据源为 : class com.zaxxer.hikari.HikariDataSource , 我们并没有手动配置

我们来全局搜索一下,找到数据源的所有自动配置都在 :DataSourceProperties 文件下;我们可以来探究下这里自动配置的原理以及能配置哪些属性;

可以看出 Spring Boot 2.1.7 默认使用 com.zaxxer.hikari.HikariDataSource 数据源,而以前版本,如 Spring Boot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源;

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

关于数据源我们并不做介绍,有了数据库连接,显然就可以 CRUD 操作数据库了。

Crud操作

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

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

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

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

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

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

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

测试

@RestController
public class JdbcController {

    //JdbcTemplate 是 core 包的核心类,用于简化 JDBC操作,还能避免一些常见的错误,如忘记关闭数据库连接
    //Spring Boot 默认提供了数据源,默认提供了 org.springframework.jdbc.core.JdbcTemplate
    //JdbcTemplate 中会自己注入数据源,使用起来也不用再自己来关闭数据库连接
    @Autowired
    JdbcTemplate jdbcTemplate;

    //查询student表中所有数据
    //List 中的1个 Map 对应数据库的 1行数据
    //Map 中的 key 对应数据库的字段名,value 对应数据库的字段值
    @GetMapping("/userList")
    public List<Map<String, Object>> userList(){
        String sql = "select * from user";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        return maps;
    }
    
    //新增一个用户
    @GetMapping("/addUser")
    public String addUser(){
        //插入语句
        String sql = "insert into mybatis.user(id, name, pwd) values (4,'小明','123456')";
        jdbcTemplate.update(sql);
        //查询
        return "addUser-ok";
    }

    //修改用户信息
    @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] = "小明2";
        objects[1] = "zxcvbn";
        jdbcTemplate.update(sql,objects);
        //查询
        return "updateUser-ok";
    }

    //删除用户
    @GetMapping("/delUser/{id}")
    public String delUser(@PathVariable("id") int id){
        //插入语句
        String sql = "delete from user where id=?";
        jdbcTemplate.update(sql,id);
        //查询
        return "delUser-ok";
    }    
}

页面访问测试,OK!

原理探究 :

org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration 数据源配置类作用 :根据逻辑判断之后,添加数据源;

SpringBoot默认支持以下数据源:

com.zaxxer.hikari.HikariDataSource (Spring Boot 2.0 以上,默认使用此数据源)

org.apache.tomcat.jdbc.pool.DataSource

org.apache.commons.dbcp2.BasicDataSource

可以使用 spring.datasource.type 指定自定义的数据源类型,值为 要使用的连接池实现的完全限定名。默认情况下,它是从类路径自动检测的。

@Configuration
@ConditionalOnMissingBean({DataSource.class})
@ConditionalOnProperty(
    name = {"spring.datasource.type"}
)
static class Generic {
    Generic() {
    }

    @Bean
    public DataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }
}

自定义数据源 DruidDataSource

DRUID 简介

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

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

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

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

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

引入数据源

1、添加上 Druid 数据源依赖。
第一步需要在应用的 pom.xml 文件中添加上 Druid 数据源依赖,而这个依赖可以从 Maven 仓库官网 Maven Repository 中获取

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

查看项目依赖,导入成功!
在这里插入图片描述

2、切换数据源;之前已经说过 Spring Boot 2.0 以上默认使用 com.zaxxer.hikari.HikariDataSource 数据源,但可以 通过 spring.datasource.type 指定数据源。

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

在这里插入图片描述3、数据源切换之后,在测试类中注入 DataSource,然后获取到它,输出一看便知是否成功切换;
在这里插入图片描述
4、切换成功!既然切换成功,就可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数 等设置项;可以查看源码

我们可以配置一些参数来测试一下;

spring:
  datasource:
    username: root
    password: 123456
    #?serverTimezone=UTC解决时区的报错
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.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 的依赖
log4j日志依赖

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

6、现在需要程序员自己为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了;我们需要 自己添加 DruidDataSource 组件到容器中,并绑定属性;

package com.kuang.springdata.config;

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

import javax.sql.DataSource;

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

}

7、去测试类中测试一下;看是否成功!

public class SpringbootDemoDataApplicationTests {

    //注入数据源
    @Autowired
    DataSource dataSource;

    @Test
    public void contextLoads() throws SQLException {
        //看一下默认数据源
        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();
    }
}

输出结果 :可见配置参数已经生效
在这里插入图片描述

配置 Druid 数据源监控

Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面。

所以第一步需要设置 Druid 的后台管理页面,比如 登录账号、密码 等;配置后台管理;

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

    Map<String, String> initParams = new HashMap<>();
    initParams.put("loginUsername", "admin"); //后台管理界面的登录账号
    initParams.put("loginPassword", "123456"); //后台管理界面的登录密码

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

    //设置初始化参数
    bean.setInitParameters(initParams);
    return bean;
    //这些参数可以在 com.alibaba.druid.support.http.StatViewServlet 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
}

配置完毕后,我们可以选择访问 : http://localhost:8080/druid/login.html
在这里插入图片描述
进入之后
在这里插入图片描述

配置 Druid web 监控 filter

这个过滤器的作用就是统计 web 应用请求中所有的数据库信息,比如 发出的 sql 语句,sql 执行的时间、请求次数、请求的 url 地址、以及seesion 监控、数据库表的访问次数 等等。

//配置 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/*");
    bean.setInitParameters(initParams);

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

配置完毕后,我们可以启动来进行测试!

我们发送一条sql语句,然后来看一下后台的消息;
在这里插入图片描述

测试OK!

SpringBoot 整合mybatis

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

Maven仓库地址:https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter/2.1.1
在这里插入图片描述
1. 导入mybatis所需要的依赖

<!-- 引入 myBatis,这是 MyBatis官方提供的适配 Spring Boot 的,而不是Spring Boot自己的-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.0</version>
</dependency>

2.配置数据库连接信息

spring:
  datasource:
    username: root
    password: 123456
    #?serverTimezone=UTC解决时区的报错
    url: jdbc:mysql://localhost:3306/springboot?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

我们这里就是用默认的数据源了;先去测试一下连接是否成功!

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootDemoMybatisApplicationTests {

    @Autowired
    DataSource dataSource;

    @Test
    public void contextLoads() throws SQLException {

        System.out.println("数据源>>>>>>" + dataSource.getClass());
        Connection connection = dataSource.getConnection();
        System.out.println("连接>>>>>>>>>" + connection);
        System.out.println("连接地址>>>>>" + connection.getMetaData().getURL());
        connection.close();
    }
}

3、查看输出结果,数据库配置OK!

4、创建实体类, 导入 Lombok!

package com.kuang.mybatis.pojo;

public class User {

    private int id;
    private String name;
    private String pwd;

    public User() {
    }

    public User(int id, String name, String pwd) {
        this.id = id;
        this.name = name;
        this.pwd = pwd;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", pwd='" + pwd + '\'' +
                '}';
    }

}

5、配置Mapper接口类

  • @Mapper的作用

    • 表示本类是一个 MyBatis 的 Mapper,等价于以前 Spring 整合 MyBatis 时的 Mapper 接口

      • 为了把mapper这个DAO交给Spring管理
    • 添加了@Mapper注解之后这个接口在编译时会生成相应的实现类

    • 从mybatis3.4.0开始加入了@Mapper注解,目的 就是为了不再写mapper映射文件

    • @Mapper注解标记这个接口作为一个映射接口

在这里插入图片描述

  • @Mapper和@Repository的使用
    • 都是把mybatis的接口层进行注入,
    • 区别是使用@Repository时需要在启动函数上加@MapperScan进行扫描操作,且使用@MapperScan时也可以不用接口层写@Repository。
    • 而@Mapper可以单独使用。
    • @Repository是spring的注解,@Mapper是ibatis的注解,@MapperScan是mybatis和spring整合的注解。
package com.kuang.mybatis.pojo.mapper;

import com.kuang.mybatis.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
//核心注解不能丢,@Mapper没有这个,自动配置无效,@Repository没有这个,不归spring管理

@Mapper//表示这是一个mybatis的mapper类:Dao;指定这是一个操作数据库的mapper
@Repository
public interface UserMapper {

    //选择全部用户
    List<User> selectUser();
    //根据id选择用户
    User selectUserById(int id);
    //添加一个用户
    int addUser(User user);
    //修改一个用户
    int updateUser(User user);
    //根据id删除用户
    int deleteUser(int id);

}

6、Mapper映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.kuang.mybatis.pojo.mapper.UserMapper">

    <select id="selectUser" resultType="User">
    select * from user
  </select>

    <select id="selectUserById" 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、SpringBoot 整合!

以前 MyBatis 未与 spring 整合时,配置数据源、事务、连接数据库的账号、密码等都是在 myBatis 核心配置文件中进行的
myBatis 与 spring 整合后,配置数据源、事务、连接数据库的账号、密码等就交由 spring 管理。因此,在这里我们即使不使用mybatis配置文件也完全ok!
既然已经提供了 myBatis 的映射配置文件,自然要告诉 spring boot 这些文件的位置

#指定myBatis的核心配置文件与Mapper映射文件
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
# 注意:对应实体类的路径
mybatis.type-aliases-package=com.kuang.mybatis.pojo

已经说过 spring boot 官方并没有提供 myBaits 的启动器,是 myBatis 官方提供的开发包来适配的 spring boot,从 pom.xml 文件中的依赖包名也能看出来,并非是以 spring-boot 开头的;

同理上面全局配置文件中的这两行配置也是以 mybatis 开头 而非 spring 开头也充分说明这些都是 myBatis 官方提供的

可以从 org.mybatis.spring.boot.autoconfigure.MybatisProperties 中查看所有配置项

@ConfigurationProperties(
    prefix = "mybatis"
)
public class MybatisProperties {
    public static final String MYBATIS_PREFIX = "mybatis";
    private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
    private String configLocation;
    private String[] mapperLocations;
    private String typeAliasesPackage;
    private Class<?> typeAliasesSuperType;
    private String typeHandlersPackage;
    private boolean checkConfigLocation = false;
    private ExecutorType executorType;
    private Class<? extends LanguageDriver> defaultScriptingLanguageDriver;
    private Properties configurationProperties;
    @NestedConfigurationProperty
    private Configuration configuration;

也可以直接去查看 官方文档

8、编写controller
maven配置资源过滤问题

<resources>
    <resource>
        <directory>src/main/java</directory>
        <includes>
            <include>**/*.xml</include>
        </includes>
        <filtering>true</filtering>
    </resource>
</resources>
package com.kuang.mybatis.controller;

import com.kuang.mybatis.pojo.User;
import com.kuang.mybatis.pojo.mapper.UserMapper;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserController {

    @Autowired
    private UserMapper userMapper;

    //选择全部用户
    @GetMapping("/selectUser")
    public String selectUser(){
        List<User> users = userMapper.selectUser();
        for (User user : users) {
            System.out.println(user);
        }

        return "ok";
    }
    //根据id选择用户
    @GetMapping("/selectUserById")
    public String selectUserById(){
        User user = userMapper.selectUserById(1);
        System.out.println(user);
        return "ok";
    }
    //添加一个用户
    @GetMapping("/addUser")
    public String addUser(){
        userMapper.addUser(new User(5,"阿毛","456789"));
        return "ok";
    }
    //修改一个用户
    @GetMapping("/updateUser")
    public String updateUser(){
        userMapper.updateUser(new User(5,"阿毛","421319"));
        return "ok";
    }
    //根据id删除用户
    @GetMapping("/deleteUser")
    public String deleteUser(){
        userMapper.deleteUser(5);
        return "ok";
    }

}

运行测试OK!

6 Spring Security

概念

  • SpringSecurity是为基于Spring的应用程序提供声明式安全保护的安全性框架。

  • SpringSecurity提供了完整的安全性解决方案,它能够在web请求级别和方法调用级别处理身份认证和授权。

  • 因为基于spring框架,所以SpringSecurity充分利用了依赖注入和面向切面的技术。

  • 为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

  • SpringSecurity从 两个角度 解决来解决安全问题

    • 使用Servlet规范中的Filter保护web请求并限制URL级别的访问。
    • SpringSecurity能够使用SpringAOP保护方法调用——借助于对象代理和使用通知,能够确保只有适当权限的用户才能访问安全保护的方法。
  • 除了 不能脱离Spring,shiro的功能它都有。
    在这里插入图片描述

  • Spring Security中文文档

6.1 Spring Security环境搭建

记住几个类:

  • WebSecurityConfigurerAdapter:自定义Security策略

  • AuthenticationManagerBuilder:自定义认证策略

  • @EnableWebSecurity:开启WebSecurity模式

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

导入依赖

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

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
</dependency>

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

6.2 用户认证和授权

路由控制

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

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

    @RequestMapping("/level1/{id}")
    private String level1(@PathVariable("id") int id) {
        return "/views/level1/" + id;
    }

    @RequestMapping("/level2/{id}")
    private String level2(@PathVariable("id") int id) {
        return "/views/level2/" + id;
    }

    @RequestMapping("/level3/{id}")
    private String level3(@PathVariable("id") int id) {
        return "/views/level3/" + id;
    }
}

到目前为止,我们的WebSecurityConfig仅包含有关如何验证用户身份的信息。Spring Security如何知道我们要求所有用户都经过身份验证?Spring Security如何知道我们想要支持基于表单的身份验证?原因是WebSecurityConfigurerAdapter在configure(HttpSecurity http)方法中提供了一个默认配置

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //首页所有人可以访问,功能页只有对应有权限的人才能访问
        http
            .authorizeRequests().antMatchers("/").permitAll()
            .antMatchers("/level1/**").hasRole("vip1")
            .antMatchers("/level2/**").hasRole("vip2")
            .antMatchers("/level3/**").hasRole("vip3");

        //没有权限就会默认到登陆页面
        http.formLogin();
    }
}

上述规定了各个级别角色所能访问的页面,运行测试一下
在这里插入图片描述
在这里插入图片描述

6.3 注销及权限控制

未分配角色前,点击任意一个页面后,自动在url后添加/login,这便是我们的配置生效了

现在继续重写一个方法protected void configure(AuthenticationManagerBuilder auth)

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //此处采用内存验证用户账号密码,也可以用数据库
    auth.inMemoryAuthentication()//各个用户分配角色权限
            .withUser("lizeyu").password("111111").roles("vip2", "vip3")
            .and()
            .withUser("root").password("111111").roles("vip1", "vip2", "vip3")
            .and()
            .withUser("guest").password("111111").roles("vip1");
}

运行后会发现报一个错误

采用普通用户登录
在这里插入图片描述在这里插入图片描述
提醒我们密码需要设置加密,完整代码如下

//认证:springboot 2.1.X 可以直接使用
//密码编码:PasswordEncoder()
//在spring security 5.0+,新增了很多的加密方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
            .withUser("lizeyu").password(new BCryptPasswordEncoder().encode("111111")).roles("vip2", "vip3")
            .and()
            .withUser("root").password(new BCryptPasswordEncoder().encode("111111")).roles("vip1", "vip2", "vip3")
            .and()
            .withUser("guest").password(new BCryptPasswordEncoder().encode("111111")).roles("vip1");
}

再次运行依然是普通用户登录,访问level1下的目录,由上可知是vip1角色才可以
在这里插入图片描述
这样目的达到,OJBK

增加注销功能

//注销
http.logout();

采用semantic-ui作为样板

<link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">

<!--注销-->
    <a class="item" th:href="@{/logout}">
       <i class="sign-out icon"></i>注销
    </a>

测试
在这里插入图片描述在这里插入图片描述
注销成功,OJBK

如果我们想让注销后跳到首页:

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

为实现不同权限的用户仅显示自己拥有权限的页面,我们引入thymeleaf与secrity整合的包

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

导入sec命名空间

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

实现未登录仅显示登录按钮,登录成功显示用户名和注销按钮

<!--如果未登录-->
<!--Authenticated 表示用户已经认证-->
<div sec:authorize="!isAuthenticated()">
    <!--未登录-->
    <a class="item" th:href="@{/toLogin}">
        <i class="address card icon"></i> 登录
    </a>
</div>

<!--如果已经登录-->
<div sec:authorize="isAuthenticated()">
    <a class="item">
        用户名:<span sec:authentication="name"></span>
    </a>
</div>

<div sec:authorize="isAuthenticated()">
    <!--注销-->
    <a class="item" th:href="@{/logout}">
        <i class="sign-out icon"></i>注销
    </a>
</div>

在这里插入图片描述

此时会发现一个问题,我们注销后无法返回首页,会进入到一个404页面,可能原因是我们采用的提交请求是get,security会认为明文传输不安全,自动帮我们屏蔽,这时我们不能觉得我们代码错了,而是配置问题
在这里插入图片描述

//关闭防止网站攻击
http.csrf().disable();

OJBK

实现不同权限用户不同页面

sec:authorize="hasRole('vip2')"          <!--加入显示标签内即可-->

在这里插入图片描述
这样我们的权限控制就完成了

6.4 记住我及首页定制

记住我功能实现

//记住我功能实现
http.rememberMe();

在这里插入图片描述
定制我们的登录页面

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

ok!

在自定义登录界面添加记住我按钮

//自定义记住我标签的name
http.rememberMe().rememberMeParameter("remember");
<div class="field">
   <input type="checkbox" name="remember-me">记住我
</div>

如果不自定义记住我的参数,观看源码可知默认的参数名为remember-me,即可自动后台帮我们设置cookie中记住我的参数

7 Shiro

概念

  • 目前,使用 Apache Shiro 的人越来越多,因为它相 当简单,对比 Spring
    Security,可能没有 Spring Security 做的功能强大,但是在实际工作时 可能并不需要那么复杂的东西,所以使用小而简单的
    Shiro 就足够了。

  • Apache Shiro是一个强大且易用的Java安全框架,

  • Shiro是Apache 的一个开源项目,前身是JSecurity 项目,始于2003年初。

  • 相比较Spring Security,shiro有小巧、简单、易上手等的优点。

  • shiro权限的操作粒度能控制在 路径按钮 上,数据粒度通过 sql实现

  • Shrio简单够用。至于OAuth,OpenID 站点间统一登录功能,

  • 现如今单点登录很多已经通过cookies实现。因此Shiro完全能够胜任平时项目的安全认证控制。

  • 使用Shiro的易于理解 API,你可以快速、轻松地获取任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序

  • Shiro 可以为 任何应用 提供安全保障 - 从命令行应用、移动应用到大型网络及企业应用。

  • shiro 解决了 应用安全 的四要素:(主要功能)

    • 认证 - 用户身份识别,常被称为用户“登录”;
    • 授权 - 访问控制;
    • 密码加密 - 保护或隐藏数据防止被偷窥;
    • 会话管理 - 每用户相关的时间敏感的状态。
    • 同时,Shiro另外支持了一些辅助特性:如 Web 应用安全、单元测试和多线程,它们的存在强化了上面提到的四个要素。

Shiro优势

1、易于使用 - 易用性是这个项目的最终目标。应用安全有可能会非常让人糊涂,令人沮丧,并被认为是“必要之恶”【译注:比喻应用安全方面的编程。】。若是能让它简化到新手都能很快上手,那它将不再是一种痛苦了。

2、广泛性 - 没有其他安全框架可以达到 Apache Shiro 宣称的广度,它可以为你的安全需求提供“一站式”服务。

3、灵活性 - Apache Shiro 可以工作在任何应用环境中。虽然它工作在 Web、EJB 和 IoC 环境中,但它并不依赖这些环境。Shiro 既不强加任何规范,也无需过多依赖。

4、Web 能力 - Apache Shiro 对 Web 应用的支持很神奇,允许你基于应用 URL 和 Web 协议(如 REST)创建灵活的安全策略,同时还提供了一套控制页面输出的 JSP 标签库。

5、可插拔 - Shiro 干净的 API 和设计模式使它可以方便地与许多的其他框架和应用进行集成。你将看到 Shiro 可以与诸如 Spring、Grails、Wicket、Tapestry、Mule、Apache Camel、Vaadin 这类 第三方框架 无缝集成。

6、支持 - Apache Shiro 是 Apache 软件基金会成员,这是一个公认为了社区利益最大化而行动的组织。项目开发和用户组都有随时愿意提供帮助的友善成员。

Shiro特点

1、易于理解的 Java Security API;

2、简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);

3、对角色的简单的签权(访问控制),支持细粒度的签权;

4、支持一级缓存,以提升应用程序的性能;

5、内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;

6、异构客户端会话访问;

7、非常简单的加密 API;

8、不跟任何的框架或者容器捆绑,可以独立运行。

功能

在这里插入图片描述

  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用
    户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用
    户对某个资源是否具有某个权限;
  • Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信
    息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
  • Web Support:Web 支持,可以非常容易的集成到 Web 环境;
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
  • Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能
    把权限自动传播过去;
  • Testing:提供测试支持;
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
  • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录
    记住一点,Shiro 不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过
    相应的接口注入给 Shiro 即可。

Shiro官方文档

从外部查看shiro框架shiro实现原理理解
在这里插入图片描述
应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject

也就是说对于我们而言,最简单的一个 Shiro 应用
应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager; 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。

api说明
Subject主体,代表当前‘用户’ 。
这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;
所有Subject都绑定到SecurityManager,与Subject的所有交互都会委派给SecurityManager;
可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
表示与系统交互的主体,通常情况下我们理解是用户。他包含了用户安全认证的相关授权信息。
Shiro SecurityManager安全管理器;即所有与安全有关的操作都会与SecurityManager交互且它管理者所有Subject;
可以看出它是Shiro的核心,它负责与后面介绍的其它组件进行交互,可以把它看成DispathcherServlet前端控制器;管理所有用户的安全操作。
它是Shiro框架的核心,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
最终的DefaultSecurityManager覆盖了其余的SecurityMananger的功能。
Realm,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;
也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;
可以把Realm看成DataSource,即安全数据源。
充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会通过Realm中查询相关的用户及其权限信息。
常见的Realm实现有:JDBCRealm、IniRealm、PropertiesRealm,
在实际开发中通常用户的认证信息都存放在数据库中,我们可以通过JDBCRealm查询数据库认证数据,
或者通过继承AuthorizingRealm自定义Realm来获取数据库认证数据(JDBCRealm也继承了AuthorizingRealm)

内部结构框架shiro的架构理解
在这里插入图片描述
组件

  1. Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
  2. SecurityManager : 相 当 于 SpringMVC 中 的 DispatcherServlet 或 者 Struts2 中的
  3. FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
  4. Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
  5. Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
  6. Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;
  7. SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所有呢,Shiro 就抽象了一个自己的 Session来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,
    这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);
  8. SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO中可以使用 Cache 进行缓存,以提高性能;
  9. CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本
    上很少去改变,放到缓存中后可以提高访问的性能
  10. Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密

常见单词说明在这里插入图片描述

Shiro中的shiro.ini说明:
在这里插入图片描述

(1) main

提供了对根对象securityManager及其依赖对象的配置

#创建对象
securityManager=org.apache.shiro.mgt.DefaultSecurityManager 

其构造器必须是public空参构造器,通过反射创建相应的实例。

1.对象名=全限定类名 相当于调用public无参构造器创建对象

2.对象名.属性名=值 相当于调用于setter方法设置常量值

3.对象名.属性名=$对象引用 相当于调用setter方法设置对象引用

(2) users

提供了对用户/密码及其角色的配置,用户名=密码,角色1,角色2

username=password,role1,role2

例如:配置用户名/密码及其角色,格式:“用户名=密码,角色1,角色2”,角色部分可省略。如:

[users] 
zhang=123,role1,role2 
wang=123 

(3) roles

提供了角色及权限之间关系的配置,角色=权限1,权限2 role1 = permission1 , permission2

例如:配置角色及权限之间的关系,格式:“角色=权限1,权限2”;如:

[roles] 
role1=user:create,user:update 
role2=*  

(4) urls

用于web,提供了对web url拦截相关的配置,url=拦截器[参数],拦截器

/index.html = anon 
/admin/** = authc, roles[admin],perms["permission1"]

7.1 shiro搭建环境

0、数据库设计

1、sys_menu
在这里插入图片描述
2、sys_role
在这里插入图片描述
3、sys_role_menu
在这里插入图片描述
4、sys_user
在这里插入图片描述在这里插入图片描述
5、sys_role_user
在这里插入图片描述

1、添加依赖
<dependencies>
  <dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-core</artifactId>
	<version>1.1.0</version>
</dependency>
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-simple</artifactId>
	<version>1.6.1</version>
	<scope>test</scope>
</dependency>

<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.12</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>commons-logging</groupId>
	<artifactId>commons-logging</artifactId>
	<version>1.2</version>
</dependency>
</dependencies>
2、 添加shiro.ini文件
[users]
root=123456
# 账号为root 密码是123456
3、认证操作
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;

/**
 * @author Administrator
 *shiro的第一个入门案例
 */
public class HelloTest {
	public static void main(String[] args) {
		//1.加载配置文件获取Factory对象
		Factory<SecurityManager> factory=new IniSecurityManagerFactory("classpath:shiro.ini");
		//2.获取SecurityManager对象
		SecurityManager securityManager =factory.getInstance();
		//3.将SecurityManager添加到系统
		SecurityUtils.setSecurityManager(securityManager);
		//4.通过SecurityManager获取Subject对象
		Subject subject=SecurityUtils.getSubject();
		//账号密码是客户端提交的数据
		AuthenticationToken token=new UsernamePasswordToken("root","123456");
		//5.实现认证操作
		try{
			subject.login(token);
			System.out.println("认证成功");
		}catch(UnknownAccountException e){
			System.out.println("账号输入错误。,,,");
		}catch (IncorrectCredentialsException e) {
			System.out.println("密码输入错误。。。");
		}		
	}
}
4、实现

在这里插入图片描述

7.2 Shiro的Subject分析

subject 被Shiro 描述为一个主体,对于web应用来说,可以简单理解为用户。

这里我们来阐述一个Shiro设计的重要理念,即以主体为展开的安全体系构建。引用一段话:

在考虑应用安全时,你最常问的问题可能是“当前用户是谁?”或“当前用户允许做 X 吗?”。当我们写代码或设计用户界面时,问自己这些问题很平常:应用通常都是基于用户故事构建的,并且你希望功能描述(和安全)是基于每个用户的。所以,对于我们而言,考虑应用安全的最自然方式就是基于当前用户。Shiro 的 API 用它的 Subject 概念从根本上体现了这种思考方式。

在应用程序中,我们可以在任何地方获取当前操作的用户主体:

import org.apache.shiro.subject.Subject;
import org.apache.shiro.SecurityUtils;
...
Subject currentUser = SecurityUtils.getSubject();

获得Subject 后,通过这个对象,我们可以对其进行绝大多数安全操作:登录、登出、访问会话、执行授权检查等。

Shiro 的api非常直观,它反映了开发者以“每个用户” 思考安全控制的自然趋势。

7.3 SpringBoot整合Shiro

1、Shiro的配置类
2、自定义Realm

通过上面我们发现仅仅将数据信息定义在ini文件中我们实际开发环境有很大不兼容,所以我们希望能够自己定义Realm。

1. 自定义Realm的实现

创建一个java文件继承AuthorizingRealm类,重写两个抽象方法:

public class SecurityRealm  extends AuthorizingRealm{

	/**
	 * 认证的方法
	 * 就是我们在测试代码中 定义的UserPassWoldToken对象:有我们保存的需要验证的账号密码信息
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		UsernamePasswordToken t=(UsernamePasswordToken) token;
		//获取登录的账号
		String username=t.getUsername();
		System.out.println("登录的账号:"+username);
		//通过jdbc去数据库中查询该账号对应的记录
		if(!"root".equals(username)){
			//账号不存在
			return null;
		}
		//数据库中查询的密码是123456
		String password="123456";
		//身份信息(可以是账号也可以是对象) 密码 realmName(自定义)
		return new SimpleAuthenticationInfo(username,password,"tang");
	}
	
	/**
	 * 授权方法
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
		// TODO Auto-generated method stub
		return null;
	}
}
方法名说明
doGetAuthentictionInfo完成账号认证的方法
doGetAuthorizationInfo完成用户授权的方法
2. 配置ini.xml文件:
[main]
#自定义 realm
customRealm=com.sxt.realm.SecurityRealm
#将realm设置到securityManager
securityManager.realms=$customRealm
3. 测试(代码跟上面的一样)
//shiro的第一个入门案例
public class HelloTest {
	public static void main(String[] args) {
		//1.加载配置文件获取Factory对象
		Factory<SecurityManager> factory=new IniSecurityManagerFactory("classpath:shiro.ini");
		//2.获取SecurityManager对象
		SecurityManager securityManager =factory.getInstance();
		//3.将SecurityManager添加到系统
		SecurityUtils.setSecurityManager(securityManager);
		//4.通过SecurityManager获取Subject对象
		Subject subject=SecurityUtils.getSubject();
-----------------------------------------------------------------------------------
		//账号密码是客户端提交的数据
		AuthenticationToken token=new UsernamePasswordToken("root","123456");
		//5.实现认证操作
		try{
			subject.login(token);
			System.out.println("认证成功");
		}catch(UnknownAccountException e){
			System.out.println("账号输入错误。,,,");
		}catch (IncorrectCredentialsException e) {
			System.out.println("密码输入错误。。。");
		}		
	}
}

在这里插入图片描述
原理分析

为什么要继承AuthorizingRealm?

通过分析认证的流程,我们发现在认证的过程中核心代码是:
在这里插入图片描述
核心方法是 doGetAuthenticationInfo(token)

在Realm的结构中
在这里插入图片描述

  • AuthorizingRealm和AuthenticatingRealm都提供的有doGetAuthenticationInfo(token)的抽象方法。
  • 但是AuthenticatingRealm中要重写的抽象方法太多而AuthorizingRealm只需要重写两个方法,
  • 且这两个方法都是我们需要使用的。故选择继承AuthorizingRealm

自定义的Realm什么时候被调用的?
在这里插入图片描述
密码验证什么时候执行的?

注意:自定义Realm中只完成了账号的认证。密码认证还是在AuthenticatingRealm中完成的,只是我们在自定义Realm中完成了密码的设置。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

7.4 Shiro实现登录拦截

public String testShiroLogin(HttpServletRequest request) {
    Subject subject = SecurityUtils.getSubject();
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    UsernamePasswordToken upt = new UsernamePasswordToken(username, password);
    subject.login(upt);
    return "success";
}

在这里插入图片描述

7.5 Shiro实现用户认证

虽然有些武断,但是一般web 应用认证就是登录功能。也就是说,当用户使用应用进行认证时,他们就在证明他们就是自己所说的那个人。
在这里插入图片描述

  • 这是一个典型的三步过程:
1、收集用户身份信息,成为当事人(principal),以及身份的支持证明,称为证书(Credential)。
2、将当事人和证书提交给系统。
3、如果提交的证书与系统期望的该用户身份(当事人)匹配,该用户就被认为是经过认证的,反之则被认为未经认证的。
@Test
public void testHelloworld() {
    //1、获取 SecurityManager 工厂,此处使用 Ini 配置文件初始化 SecurityManager
    Factory<org.apache.shiro.mgt.SecurityManager> factory =
    new IniSecurityManagerFactory("classpath:shiro.ini");
    //2、得到 SecurityManager 实例 并绑定给 SecurityUtils
    org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
    SecurityUtils.setSecurityManager(securityManager);
    //3、得到 Subject 及创建用户名/密码身份验证 Token(即用户身份/凭证)
    Subject subject = SecurityUtils.getSubject();
    // UsernamePasswordToken 继承
    UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
    try {
    //4、登录,即身份验证
    subject.login(token);
    } catch (AuthenticationException e) {
    //5、身份验证失败
    }
    Assert.assertEquals(true, subject.isAuthenticated()); //断言用户已经登录
    //6、退出
    subject.logout();
}
try {
    currentUser.login(token);
} catch (IncorrectCredentialsException ice) {} catch (LockedAccountException lae) {}catch (AuthenticationException ae) {} 

如果没有抛出任何异常,则证明 Subject 登录成功,就被认为是已认证的。

1、UsernamePasswordToken 实现HostAuthenticationToken和RemeberAuthenticationToken,HostAuthenticationToken实现AuthenticationToken

2、首先调用 **Subject.login(token)**进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils. setSecurityManager()设置;

3、SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;SecurityManager j接口继承Authenticator、Authrizer、sessionManage接口

4、Authenticator 才是真正的身份验证者Shiro API 中核心的身份认证入口点,此处可以自
定义插入自己的实现;

5、Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认
ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;

6、Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。
在这里插入图片描述
SecurityManager实现类结构关系
在这里插入图片描述

7.6 Shiro请求授权实现

授权实质上就是访问控制,控制已认证的用户能够访问应用的哪些内容,如资源、页面等。

  • 基于角色的访问控制(隐式角色)

  • 基于资源的访问控制(显示角色)

多数用户执行访问控制是通过 角色 + 权限 的概念来完成的。角色是所有用户个体的一个分组,如管理员、普通用户、商家等;而权限 则表示具体能够操作的行为,比如查询所有用户、删除某些用户、修改信息等等,是与具体应用资源直接挂钩的。

用户、角色和 权限三者往往通过 角色 来进行转换,用户和权限之间通常不进行直接绑定:
在这里插入图片描述
实现方式:

// 1. 编程式(单个和多个角色)
subject.hasRole(“admin”)Array.aslist("admin1","admin2");

//2. 注解
@RequiresRoles("admin")

//3. 页面控制
<shiro:hasRole name="admin">

//4. shiro配置文件中配置
 <property name="filterChainDefinitions">
            <value>
                /commons/** = anon
                /plugins/** = anon
                /assets/** = anon
                /css/** = anon
                /js/** = anon
                /img/** = anon
                /fonts/** = anon
                /bootstrap/** = anon
                /login = anon
                /interface/** = anon
                /** = user                
            </value>
</property>
public String testShiro(HttpServletRequest request) {
    Subject subject = SecurityUtils.getSubject();
    String username = subject.getPrincipal().toString();
    subject.isPermitted("admin:test");
    return username + "请求成功";
}

在这里插入图片描述
shiro 拦截器规则:
在这里插入图片描述在这里插入图片描述在这里插入图片描述
实现流程:
在这里插入图片描述

常用的权限注解
  • @RequiresAuthentication
    表示当前 Subject 已经通过 login 进行了身份验证;即 Subject. isAuthenticated()返回 true
  • @RequiresUser
    表示当前 Subject 已经身份验证或者通过记住我登录的。
  • @RequiresGuest
    表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份
  • @RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)
    表示当前 Subject 需要角色 admin 和 user。
  • @RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)
    表示当前 Subject 需要权限 user:a 或 user:b

7.7 Shiro整合Thymeleaf

第一步:引入依赖

<dependency> 
    <groupId>com.github.theborakompanioni</groupId> 
    <artifactId>thymeleaf-extras-shiro</artifactId> 
    <version>2.0.0</version> 
</dependency>

第二步:在Shiro的Config文件中对设置进行相应的修改:

@Bean(name = "shiroDialect") //自定义标签    
public ShiroDialect shiroDialect(){     
    return new ShiroDialect(); 
}

添加这段代码的目的就是为了在thymeleaf中 使用shiro的自定义tag

第三步:现在基本上所有使用shiro-tag的条件都具备了,现在给出前端的代码示例:

<!DOCTYPE html> 
<html xmlns="http://www.w3.org/1999/xhtml" 
      xmlns:th="Thymeleaf"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head> 
<meta charset="UTF-8" /> 
<title>Insert title here</title>
</head> 
<body> 
<h3>index</h3>
 
<!-- 验证当前用户是否为“访客”,即未认证(包含未记住)的用户。 --> 
<p shiro:guest="">Please <a href="login.html">login</a></p> 
 
<!-- 认证通过或已记住的用户。 --> 
<p shiro:user=""> 
Welcome back John! Not John? Click <a href="login.html">here</a> to login. 
</p> 
 
<!-- 已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在。 -->
 <p shiro:authenticated=""> 
Hello, <span shiro:principal=""></span>, how are you today? 
</p> 
<a shiro:authenticated="" href="updateAccount.html">Update your contact information</a> 
 
<!-- 输出当前用户信息,通常为登录帐号信息。 --> 
<p>Hello, <shiro:principal/>, how are you today?</p> 
  
<!-- 未认证通过用户,与authenticated标签相对应。与guest标签的区别是,该标签包含已记住用户。 --> 
<p shiro:notAuthenticated=""> 
Please <a href="login.html">login</a> in order to update your credit card information. 
</p> 
 
<!-- 验证当前用户是否属于该角色。 -->
<a shiro:hasRole="admin" href="admin.html">Administer the system</a><!-- 拥有该角色 --> 
 
<!-- 与hasRole标签逻辑相反,当用户不属于该角色时验证通过。 --> 
<p shiro:lacksRole="developer"><!-- 没有该角色 --> 
Sorry, you are not allowed to developer the system. 
</p> 
 
<!-- 验证当前用户是否属于以下所有角色。 --> 
<p shiro:hasAllRoles="developer, 2"><!-- 角色与判断 --> 
You are a developer and a admin. 
</p> 
 
<!-- 验证当前用户是否属于以下任意一个角色。 --> 
<p shiro:hasAnyRoles="admin, vip, developer,1"><!-- 角色或判断 -->
 You are a admin, vip, or developer. </p> 
 
<!--验证当前用户是否拥有指定权限。 --> 
<a shiro:hasPermission="userInfo:add" href="createUser.html">添加用户</a><!-- 拥有权限 --> 
 
<!-- 与hasPermission标签逻辑相反,当前用户没有制定权限时,验证通过。 --> 
<p shiro:lacksPermission="userInfo:del"><!-- 没有权限 --> 
Sorry, you are not allowed to delete user accounts. </p> 
 
<!-- 验证当前用户是否拥有以下所有角色。 --> 
<p shiro:hasAllPermissions="userInfo:view, userInfo:add"><!-- 权限与判断 --> 
You can see or add users. </p> 
 
<!-- 验证当前用户是否拥有以下任意一个权限。 --> 
<p shiro:hasAnyPermissions="userInfo:view, userInfo:del"><!-- 权限或判断 --> 
You can see or delete users. </p>
 
<a shiro:hasPermission="pp" href="createUser.html">Create a new User</a>
 
</body> 
</html>

这里注意一个细节,在html界面的顶部加一个tag标签引入:

xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"

7.8 shiro-加密

加密,是以某种特殊的算法改变原有的信息数据,使得未授权的用户即使获得了已加密的信息,但因不知解密的方法,任然无法了解信息的内容

1、概念:

数据加密的基本过程就是对原来为明文的文件或数据按照某种算法进行处理,使其成为不可读的一段代码,通常称为“密文”,使其只能在输入相应的密匙之后才能显示出本来内容,通过这样的途径来达到保护数据不被非法人窃取、阅读的目的。该过程的逆过程为解密,即将该编码信息转换为其原来数据的过程。

2、加密分类:

(1)、对称加密

双方使用的同一个密匙,既可以加密又可以解密,这种加密方法称为对称加密,也称单密匙加密。

(2)、非对称加密

一对密匙由公钥和私钥组成(可以使用很多对密匙)。私钥解密公钥加密数据,公钥解密私钥加密数据(私钥公钥可以互相加密解密)。

3、加密算法分类

(1)、单项加密

单项加密是不可逆的,也就是只能加密,不能解密。通常用来传输类型用户名和密码,直接将加密后的数据提交到后台,因为后台不需要知道用户名和密码,可以直接将接收到的加密后的数据存储到数据库

(2)、双向加密

通常分为对称性加密算法和非对称性加密算法,对于对称性加密算法,信息接收双方都需事先知道密匙和加解密算法且其密匙是相同的,之后便是对数据进行 加解密了。非对称算法与之不同,发送双方A,B事先均生成一堆密匙,然后A将自己的公有密匙发送给B,B将自己的公有密匙发送给A,如果A要给B发送消 息,则先需要用B的公有密匙进行消息加密,然后发送给B端,此时B端再用自己的私有密匙进行消息解密,B向A发送消息时为同样的道理。

4、常见的算法
在这里插入图片描述

MD5的使用:
import org.apache.shiro.crypto.hash.Md5Hash;

public class Md5HashTest {
	public static void main(String[] args) {
		// 对单个信息加密
		Md5Hash md5 = new Md5Hash("123456");
		System.out.println(md5.toString());
		// 加密添加盐值 增大解密难度
		md5 = new Md5Hash("123456","aaa");
		System.out.println(md5.toString());
		// 加密添加盐值 增大解密难度  迭代1024次
		md5 = new Md5Hash("123456","aaa",1024);
		System.out.println(md5);	
	}
}

输出的结果:
在这里插入图片描述

盐值的作用:

使用MD5存在一个问题,相同的password生成的hash值是相同的,如果两个用户设置了相同的密码,那么数据库中会存储两个相同的值,这是极不安全的,加Salt可以在一定程度上解决这一问题,所谓的加Salt方法,就是加点‘佐料’。其基本想法是这样的,当用户首次提供密码时(通常是注册时)由系统自动往这个密码里撒一些‘佐料’,然后在散列,而当用户登录时,系统为用户提供的代码上撒上相同的‘佐料’,然后散列,再比较散列值,来确定密码是否正确。

加盐的原理:
给原文加入随机数生成新的MD5的值

shiro中使用MD5加密

1.认证方法中修改

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
	// 获取账号信息
	String principal = (String) token.getPrincipal();
	// 正常逻辑此处应该根据账号去数据库中查询,此处我们默认账号为 root 密码123456
	// 验证账号
	if(!"root".equals(principal)){
		// 账号错误
		return null;
	}
	//String pwd = "123456";
	// 12345 根据 盐值 aaa 加密获取的密文
	//88316675d7882e3fdbe066000273842c  1次迭代的密文
	//a7cf41c6537065fe724cc9980f8b5635  2次迭代的密文
	String pwd = "88316675d7882e3fdbe066000273842c";
	// 验证密码
	AuthenticationInfo info = new SimpleAuthenticationInfo(
			principal, pwd,new SimpleByteSource("aaa"),"myrealm");
	return info;
}

在这里插入图片描述
2.ini.xml文件的修改:

[main]
#定义凭证匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#散列算法
credentialsMatcher.hashAlgorithmName=md5
#散列次数
credentialsMatcher.hashIterations=1

#将凭证匹配器设置到realm
customRealm=com.dpb.realm.MyRealm
customRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$customRealm

3.测试:

@Test
public void test() {
	// 1.获取SecurityManager工厂对象
	Factory<SecurityManager> factory = 
			new IniSecurityManagerFactory("classpath:shiro.ini");
	// 2.通过Factory对象获取SecurityManager对象
	SecurityManager securityManager = factory.getInstance();
	// 3.将SecurityManager对象添加到当前运行环境中
	SecurityUtils.setSecurityManager(securityManager);
	
	// 4.获取Subject对象
	Subject subject = SecurityUtils.getSubject();
	AuthenticationToken token = new UsernamePasswordToken("root", "123456");
	// 登录操作
	try {
		subject.login(token);
	} catch (UnknownAccountException e) {
		System.out.println("账号出错...");
	} catch(IncorrectCredentialsException e){
		System.out.println("密码出错...");
	}
	// 获取登录的状态
	System.out.println(subject.isAuthenticated());
}

4.迭代次数:
在这里插入图片描述
在这里插入图片描述
完成。

Shiro的安全加密主要使用在几个地方:
  • 散列算法:如Md5Hash、Sha256Hash、Sha512Hash、DefaultHashService等相关类都可以用来对具体数据进行散列值计算。
  • 加密/解密:Shiro 还提供对称式加密 / 解密算法的支持,如 AES、Blowfish 等。可以使用AesCipherService、BlowfishCipherService服务类进行相关数据的加密和解密操作
  • PasswordService/CredentialsMatcher:Shiro 提供了 PasswordService 及 CredentialsMatcher 用于提供加密密码及验证密码服务。
public interface PasswordService {
    //加密
    String encryptPassword(Object var1) throws IllegalArgumentException;
    //匹配
    boolean passwordsMatch(Object var1, String var2);
}

常用的PasswordService 实现是 DefaultPasswordService

public interface CredentialsMatcher {
    //匹配用户输入的token的凭证(未加密)与系统提供的凭证(已加密)
    boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info);
}

常用的CredentialsMatcher实现是PasswordMatcher和HashedCredentialsMatcher。

(1)PasswordMatcher

DefaultPasswordService 配合 PasswordMatcher 实现简单的密码加密与验证服务,PasswordMatcher中有一个PasswordService类型的成员变量,可以通过setPasswordService()方法指定PasswordService。

下面是自定义的Realm中指定CredentialsMatcher的代码:

//MyCsutomRealm是继承了AuthorizingRealm类的自定义Realm
public MyCustomRealm() {
    PasswordMatcher passwordMatcher = new PasswordMatcher();
    passwordMatcher.setPasswordService(defaultPasswordService);
    this.setCredentialsMatcher(passwordMatcher);
}

**注:**在实际项目使用中为了方便,利用Spring的依赖注入,我们可以先将DefaultPasswordService通过注解或者xml的方式配置成一个bean对象,在使用时直接注入一个 PasswordService 来加密密码,实际使用时需要在 Service 层使用 PasswordService 加密密码并存到数据库。可以采用@Autowired注入,或者在配置文件中利用constructor-arg标签通过构造函数注入、或者在配置文件中利用property标签通过set方法注入。

(2)HashedCredentialsMatcher

Shiro 提供了 CredentialsMatcher 的散列实现 HashedCredentialsMatcher,和之前的 PasswordMatcher 不同的是,它只用于密码验证,且可以提供自己的盐,而不是随机生成盐,且生成密码散列值的算法需要自己写,因为能提供自己的盐。
因为HashedCredentialsMatcher只能用于密码验证。因此在实际的项目使用中常用PasswordMatcher作为Realm中的CredentialsMatcher。

7.9 会话管理

  • Shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如 web 容器 tomcat),不管 JavaSE 还是 JavaEE 环境都可以使用,提供了会话管理、会话事件监听、会话存储 / 持久化、容器无关的集群、失效 / 过期支持、对 Web 的透明支持、SSO 单点登录的支持等特性。即直接使用 Shiro 的会话管理可以直接替换如 Web 容器的会话管理。
会话

Shiro中提供了Session接口,并且有多个实现,如DelegatingSession、HttpServletSession、SimpleSession等多种实现,我们也可以实现Session接口,自定义Session。

  • 获取当前用户的Session 对象
Session session = subject.getSession();
Session session = subject.getSession(boolean create);
  • Shiro Session 的一些方法
Session session = subject.getSession();
session.getAttribute("key", someValue); 
Date start = session.getStartTimestamp();
Date timestamp = session.getLastAccessTime(); 
session.setTimeout(millis);
会话管理器
  • 会话管理器管理着应用中所有 Subject 的会话的创建、维护、删除、失效、验证等工作。
  • 是 Shiro 的核心组件,顶层组件 SecurityManager 直接继承了 SessionManager。
  • SessionsSecurityManager 是SecurityManager的一种实现,它内部定义了SessionManager成员变量。
  • 最常使用的DefaultSecurityManager 、 DefaultWebSecurityManager 两种SecurityManager都继承了 SessionsSecurityManager。

Shiro 提供了三个默认实现:

  • DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;

  • DefaultWebSessionManager:继承DefaultSessionManager用于 Web 环境的实现,可以替代

    ServletContainerSessionManager,自己维护着会话,直接废弃了 Servlet 容器的会话管理。

  • ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 Web 环境,其直接使用 Servlet 容器的会话;

Session超时管理

根据Shiro提供的默认SessionManager的实现:

  • DefaultSessionManager和DefaultWebSessionManager都实现了AbstractSessionManager抽象类,AbstractSessionManager中定义的成员变量globalSessionTimeout表示Session的超时时间,我们在实际使用中,可以通过配置文件或者set方法来设置具体的Session超时时间。

  • 在ServletContainerSessionManager中的Session是HttpServletSession,该类实际上是Shiro对传统的HttpSession进行包装,本质上仍然是HttpSession。因此我们在实际使用中除了通过set方法来设置超时时间外,还可以通过web.xml配置传统session超时时间的方式来设置。
    会话监听

  • Shiro提供了SessionListener接口,我们通过实现该接口可以自定义会话监听功能。
    会话存储

  • Shiro中提供了SessionDao接口,并且定义了AbstractSessionDao抽象类,我们可以通过继承AbstractSessionDao接口,自定义Session的存储,再结合缓存工具,如:Redis,就可以实现将Session存入缓存中,如果需要用到分布式缓存,也可以利用Redis搭建分布式集群来实现。

Swagger笔记

https://mp.weixin.qq.com/s/0-c0MAgtyOeKx6qzmdUG0w
https://blog.csdn.net/qq_41978509/article/details/116104434

8 SpringBoot分布式:Dubbo+zookeeper

基础知识

分布式理论

什么是分布式系统?

在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;

分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据

分布式系统(distributed system)是建立在网络之上的软件系统。

首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。

Dubbo文档

随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。
在Dubbo的官网文档有这样一张图在这里插入图片描述

单一应用架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
在这里插入图片描述
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。

缺点:
1、性能扩展比较难
2、协同开发问题
3、不利于升级维护

垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
在这里插入图片描述
通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。
缺点: 公用模块无法重复利用,开发性的浪费

分布式服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的**分布式服务框架(RPC)**是关键。
在这里插入图片描述

流动计算架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键
在这里插入图片描述

RPC
什么是RPC?

RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。

也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;

推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b

RPC基本原理

在这里插入图片描述

步骤解析:

在这里插入图片描述
RPC两个核心模块:通讯,序列化。

Dubbo概念

1、什么是dubbo?
  • Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架
    • 核心部分包含 :(面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现)
    1. 远程通讯: 提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。
    2. 集群容错: 提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。
    3. 自动发现: 基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。
  • Dubbo是阿里巴巴开源的基于 Java 的高性能 RPC(一种远程调用) 分布式服务框架(SOA),致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
  • 说白了就是个远程服务调用的分布式框架(告别Web Service模式中的WSdl,以服务者与消费者的方式在dubbo上注册)
  • dubbo官网
    • 了解Dubbo的特性
    • 查看官方文档
2、dubbo基本概念

在这里插入图片描述
节点角色说明:

Provider: 暴露服务的服务提供方。

Consumer: 调用远程服务的服务消费方。

Registry: 服务注册与发现的注册中心。

Monitor: 统计服务的调用次调和调用时间的监控中心。

==Container==: 服务运行容器。

这点我觉得非常好,角色分明,可以根据每个节点角色的状态来确定该服务是否正常。

3、调用关系说明
  1. 服务容器负责启动,加载,运行服务提供者。
  2. 服务提供者在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

dubbo的容错性显而易见,性能方面还没有还得及测,我们系统某页面需要掉5次接口,本来想建议做个缓存,但业务关系不能采纳,还需要研究下dubbo的性能调优问题…

4、这么设计的意义?
  1. Consumer 与Provider 解偶,双方都可以横向增减节点数。

  2. 注册中心对本身可做对等集群,可动态增减节点,并且任意一台宕掉后,将自动切换到另一台

  3. 去中心化,双方不直接依懒注册中心,即使注册中心全部宕机短时间内也不会影响服务的调用

  4. 服务提供者无状态,任意一台宕掉后,不影响使用
    在这里插入图片描述

5、Dubbo能做什么?
  1. 透明化的远程方法调用,就像调用本地方法一样调用远程方法,只需简单配置,没有任何API侵入。
  2. 软负载均衡及容错机制,可在内网替代F5等硬件负载均衡器,降低成本,减少单点。
  3. 服务自动注册与发现,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。

Dubbo采用全spring配置方式,透明化接入应用,对应用没有任何API侵入,只需用Spring加载Dubbo的配置即可,Dubbo基于Spring的Schema扩展进行加载。

之前使用Web Service,我想测试接口可以通过模拟消息的方式通过soapui或LR进行功能测试或性能测试。但现在使用Dubbo,接口之间不能直接交互,我尝试通过模拟消费者地址测试,结果不堪入目,再而使用jmeter通过junit进行测试,但还是需要往dubbo上去注册,如果再不给提供源代码的前提下,这个测试用例不好写啊…

Dubbo环境搭建

点进dubbo官方文档,推荐我们使用Zookeeper 注册中心
什么是zookeeper呢?可以查看官方文档

window下安装zookeeper
  1. 下载zookeeper :地址, 我们下载3.4.14 , 最新版! 解压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: 127.0.0.1:2181(CONNECTED) 4] ls /
[zookeeper]

create –e /kuangshen 123:创建一个kuangshen节点,值为123
在这里插入图片描述
get /kuangshen:获取/kuangshen节点的值
在这里插入图片描述
我们再来查看一下节点
在这里插入图片描述

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地址:dubbo.registry.address=.........

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

mvn clean package -Dmaven.test.skip=true 

第一次打包的过程有点慢,需要耐心等待!直到成功!
在这里插入图片描述

4、执行 dubbo-admin\target 下的dubbo-admin-0.0.1-SNAPSHOT.jar

java -jar dubbo-admin-0.0.1-SNAPSHOT.jar

注意:zookeeper的服务一定要打开!

执行完毕,我们去访问一下 http://localhost:7001/ , 这时候我们需要输入登录账户和密码,我们都是默认的root-root;

登录成功后,查看界面
在这里插入图片描述
安装完成!

SpringBoot + Dubbo + zookeeper

1、框架搭建

1. 启动zookeeper !
2. IDEA创建一个空项目;
3.创建一个模块,实现服务提供者:provider-server , 选择web依赖即可
4.项目创建完毕,我们写一个服务,比如卖票的服务;

编写接口

package com.kuang.provider.service;

public interface TicketService {
    public String getTicket();
}

编写实现类

package com.kuang.provider.service;

public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "《狂神说Java》";
    }
}

5.创建一个模块,实现服务消费者:consumer-server , 选择web依赖即可

6.项目创建完毕,我们写一个服务,比如用户的服务;

编写service

package com.kuang.consumer.service;

public class UserService {
    //我们需要去拿去注册中心的服务
}

需求:现在我们的用户想使用买票的服务,这要怎么弄呢 ?

2、服务提供者
  1. 导依赖包
    将服务提供者注册到注册中心,我们需要整合Dubbo和zookeeper,所以需要导包。我们从dubbo官网进入github,看下方的帮助文档,找到dubbo-springboot,找到依赖包
<!-- Dubbo Spring Boot Starter -->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>2.7.3</version>
</dependency>    

zookeeper的包我们去maven仓库下载,zkclient

<!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient -->
<dependency>
    <groupId>com.github.sgroschupf</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.1</version>
</dependency>

【新版的坑】zookeeper及其依赖包,解决日志冲突,还需要剔除日志依赖:剔除这个slf4j-log4j12

<!-- 引入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.4.14</version>
    <!--剔除这个slf4j-log4j12-->
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>
  1. 在springboot配置文件中配置dubbo相关属性!
#当前应用名字
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#扫描指定包下服务
dubbo.scan.base-packages=com.kuang.provider.service
  1. 在service的实现类中配置服务注解,发布服务!注意导包问题
import org.apache.dubbo.config.annotation.Service;
import org.springframework.stereotype.Component;

@Service //将服务发布出去
@Component //放在容器中
public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "《狂神说Java》";
    }
}

逻辑理解 : 应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它发布在指定的注册中心中!

3、消费者
  1. 导入依赖,和之前的依赖一样;
<!--dubbo-->
<!-- Dubbo Spring Boot Starter -->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>2.7.3</version>
</dependency>
<!--zookeeper-->
<!-- https://mvnrepository.com/artifact/com.github.sgroschupf/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.4.14</version>
    <!--剔除这个slf4j-log4j12-->
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>
  1. 配置参数
#当前应用名字
dubbo.application.name=consumer-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
  1. 直接将服务的接口拿过来
    本来正常步骤是需要将服务提供者的接口打包,然后用pom文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同
    在这里插入图片描述
  2. 完善消费者的服务类
package com.kuang.consumer.service;

import com.kuang.provider.service.TicketService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;

@Service //注入到容器中
public class UserService {

    @Reference //远程引用指定的服务,他会按照全类名进行匹配,看谁给注册中心注册了这个全类名
    TicketService ticketService;

    public void bugTicket(){
        String ticket = ticketService.getTicket();
        System.out.println("在注册中心买到"+ticket);
    }
}
  1. 测试类编写;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ConsumerServerApplicationTests {

    @Autowired
    UserService userService;

    @Test
    public void contextLoads() {

        userService.bugTicket();
    }
}
4、启动测试
  1. 开启zookeeper
  2. 打开dubbo-admin实现监控【可以不用做】
  3. 开启服务者
  4. 消费者消费测试,结果:
    在这里插入图片描述
    监控中心 :
    在这里插入图片描述
    ok , 这就是SpingBoot + dubbo + zookeeper实现分布式开发;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值