月佩半琪
为什么要努力,因为喜欢的东西很贵,想去的地方很远,我爱的人超完美。

SpringBoot

1、SpringBoot: 快速入门
2、SpringBoot: 配置文件及自动配置原理

3、SpringBoot: Web开发
4、SpringBoot: Mybatis +Druid数据访问
5、SpringSecurity (安全)
6、Shiro(安全)
7、任务
8、SpringBoot分布式:Dubbo+zookeeper
9、聊聊现在和未来
100、 通用

1、SpringBoot:快速入门

什么是Spring

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

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

Spring是如何简化Java开发的

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

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

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

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

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

什么是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配置的要求

使用 Spring Boot 到底有多爽,用下面这幅图来表达

1569859952418.png

1、微服务

什么是微服务?

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

单体应用架构

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

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

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

微服务架构

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

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

这样做的好处是:

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

1569860054088.png

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

如何构建微服务

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

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

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

1569860149218.png

2、第一个SpringBoot程序(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“按钮生成项目;下载此项目
    1570257132310.png
  4. 解压项目包,并用编译器以Maven项目导入,以IntelliJ IDEA为例:
  5. 导入这个Maven项目,一路下一步即可,直到项目导入完毕。如果是第一次使用,可能速度会比较慢,需要耐心等待一切就绪
    1570257159884.png

项目结构分析

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

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

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

1570257265158.png

pom.xml 分析

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

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--父项目-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.kuang</groupId>
    <artifactId>springboot-01-hellospringboot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-01-hellospringboot</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <!--启动springboot的一些依赖-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--单元测试-->
        <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>
    <!--打包成JAR-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

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

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

编写HTTP接口

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

  2. 在包中新建一个Controller类

    package com.kuang.controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    //原理,自动装配
    @RestController
    public class HelloController {
        //按口: http://localhost : 8080/hello
        @RequestMapping("/hello")
        //调用业务,按收前端的参数!
        public String hello() {
            return "Hello World";
        }
        
    }
    
  3. 编写完毕后,从主程序启动项目,浏览器发起请求,看页面返回;

    • 控制台输出了SpringBoot 的 banner
    • 控制条输出了 Tomcat 访问的端口号!
    • 访问 hello 请求,字符串成功返回!
  4. 将项目打成jar包

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

访问 hello 请求,依旧可以得到结果

小结

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

彩蛋

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

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

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

..... (¯`v´¯)♥
.......•.¸.•´
....¸.•´
... (
 ☻/
/▌♥♥
/ \ ♥♥
             █████ █████     ███     ███████████                  ███
            ▒▒███ ▒▒███     ▒▒▒     ▒▒███▒▒▒▒▒███                ▒▒▒
             ▒▒███ ███      ████     ▒███    ▒███     ██████     ████
              ▒▒█████      ▒▒███     ▒██████████     ███▒▒███   ▒▒███
               ███▒███      ▒███     ▒███▒▒▒▒▒███   ▒███████     ▒███
              ███ ▒▒███     ▒███     ▒███    ▒███   ▒███▒▒▒      ▒███
             █████ █████    █████    ███████████    ▒▒██████     █████
            ▒▒▒▒▒ ▒▒▒▒▒    ▒▒▒▒▒    ▒▒▒▒▒▒▒▒▒▒▒      ▒▒▒▒▒▒     ▒▒▒▒▒

然后重启试试吧!

使用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,就可以看到效果了!
    1570259079467.png

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

3、运行原理探究

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的场景启动器

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

}

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

1、@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 {
    //.....
}

2、@ComponentScan

这个注解在Spring中很重要 , 它对应XML配置中的元素。@ComponentScan的功能就是自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中 ;

3、@SpringBootConfiguration

意义:SpringBoot的配置类 ;标注在某个类上 , 表示这是一个SpringBoot的配置类;

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

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

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

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

4、@EnableAutoConfiguration

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

1570275600891.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的配置文件 , 看到了很多自动配置的文件;这就是自动配置根源所在!

1570276858427.png

WebMvcAutoConfiguration

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

1570277345733.png

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

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

结论:

  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方法

2、SpringBoot:配置文件及自动配置原理

配置文件

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

  • application.properties
    • 语法结构 : key=value
  • application.yaml
    • 语法结构 :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

注入配置文件

程序实现

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

img

2. 导入配置文件处理器

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

3.编写yml 配置文件

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

package com.kuang.springbootdemo03.pojo;

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

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


/*
@ConfigurationProperties作用:
将配置文件中配置的每一个属性的值,映射到这个组件中;
告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应

只有这个组件是容器中的组件,才能使用容器提供的@ConfigurationProperties功能
*/
@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方法
    
}

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

package com.kuang.springbootdemo03.pojo;

public class Dog {
    private String name;
    private Integer age;
    
    //get、set方法
    //toString()方法  
}

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

    @Autowired
    Person person = new Person();

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

}

[复制代码](javascript:void(0)😉

运行结果

img

唠叨两句

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

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

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

settings-->FileEncodings 中配置;

img

还有,我们的类和配置文件直接关联着 , 我们使用的是@configurationProperties的方式,还有一种方式是使用@value

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

结果

img

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

img

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

结论:

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

JSR303数据校验

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

[复制代码](javascript:void(0)😉

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

    //@Value("${person.name}")
    @Email //name必须是邮箱格式
    private String name;
}

[复制代码](javascript:void(0)😉

运行结果

img

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

加载指定配置文件

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

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

name=kuangshen

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

[复制代码](javascript:void(0)😉

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

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

    ......  
}

[复制代码](javascript:void(0)😉

测试结果:

img

配置文件占位符

随机数

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

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

多环境切换

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去实现不需要创建多个配置文件,更加方便了

[复制代码](javascript:void(0)😉

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

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


---

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

[复制代码](javascript:void(0)😉

注意:如果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官方文档

分析自动配置原理

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

\2. @EnableAutoConfiguration 作用 :

  • 利用EnableAutoConfigurationImportSelector给容器中导入一些组件,导入了哪些组件呢?

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

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

  • [复制代码](javascript:void(0)😉

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        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;
    }
    

    [复制代码](javascript:void(0)😉

    这里里面有一个 SpringFactoriesLoader.loadFactoryNames() ,我们继续进去看 , 它又调用了 loadSpringFactories 方法;继续跟踪。发现它去获得了一个资源文件:"META-INF/spring.factories"

  • Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
    

    继续阅读源码 , 它将读取到的资源封装在url中,然后遍历url , 将这些url文件封装在Properties文件中;最后返回封装好的结果;他的那个ClassLoader参数,我们追踪回去,看到他就是 EnableAutoConfiguration ;img

  • 说明了这个逻辑就是 从properties中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在容器中

  • 总结一句话就是:将类路径下 META-INF/spring.factories 里面配置的所有EnableAutoConfiguration的值加入到了容器中;我们从源码中拿过来

[复制代码](javascript:void(0)😉

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.reactor.core.ReactorCoreAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityRequestMatcherProviderAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration

[复制代码](javascript:void(0)😉

每一个这样的 xxxAutoConfiguration类都是容器中的一个组件,最后都加入到容器中;用他们来做自动配置;

\3. 每一个自动配置类可以进行自动配置功能;

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

一但这个配置类生效;这个配置类就会给容器中添加各种组件;这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;

\5. 所有在配置文件中能配置的属性都是在xxxxProperties类中封装者‘;配置文件能配置什么就可以参照某个功能对应的这个属性类

[复制代码](javascript:void(0)😉

@ConfigurationProperties(
    prefix = "spring.http"
) //从配置文件中获取指定的值和bean的属性进行绑定
public class HttpProperties {
    private boolean logRequestDetails;
    private final HttpProperties.Encoding encoding = new HttpProperties.Encoding();

    public HttpProperties() {
    }

    public boolean isLogRequestDetails() {
        return this.logRequestDetails;
    }

    public void setLogRequestDetails(boolean logRequestDetails) {
        this.logRequestDetails = logRequestDetails;
    }

    public HttpProperties.Encoding getEncoding() {
        return this.encoding;
    }

    public static class Encoding {
        public static final Charset DEFAULT_CHARSET;
        private Charset charset;
        private Boolean force;
        private Boolean forceRequest;
        private Boolean forceResponse;
        private Map<Locale, Charset> mapping;
        
        、、、、、、
    }
}

[复制代码](javascript:void(0)😉

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

img

这就是自动装配的原理!

精髓:

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

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

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

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

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

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

@Conditional

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

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

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

img

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

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

#开启springboot的调试类,可以通过debug=true来查看,哪些自动配置类生效,哪些没有生效
debug: true

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

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

Unconditional classes: (没有条件的类)

输出的日志我们可以在这里看下:

img 控制台打印日志

如果你都认真看完了,那么小狂神给你点个赞 , 掌握吸收理解原理,即可以不变应万变!

3、SpringBoot:Web开发

西部开源-秦疆老师:基于SpringBoot 2.1.6 的博客教程 , 基于atguigu 1.5.x 视频优化

秦老师交流Q群号: 664386224

未授权禁止转载!编辑不易 , 转发请注明出处!防君子不防小人,共勉!

简介

好的,同学们,那么接下来呢,我们开始学习SpringBoot与Web开发,从这一章往后,就属于我们实战部分的内容了;

其实SpringBoot的东西用起来非常简单,因为SpringBoot最大的特点就是自动装配。

使用SpringBoot的步骤:

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

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

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

  • 向容器中自动配置组件 : *** Autoconfiguration
  • 自动配置类,封装配置文件的内容:***Properties

没事就找找类,看看自动装配原理,我们之后来进行一个CRUD的实验测试!

静态资源映射规则

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

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

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

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

我们先来聊聊这个静态资源映射规则;

SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置里面,我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;

比如:addResourceHandlers

[复制代码](javascript:void(0)😉

  public void addResourceHandlers(ResourceHandlerRegistry registry) {
            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");
            } else {
                Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
                CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
                if (!registry.hasMappingForPattern("/webjars/**")) {
                    this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
                }

                String staticPathPattern = this.mvcProperties.getStaticPathPattern();
                if (!registry.hasMappingForPattern(staticPathPattern)) {
                    this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
                }

            }
        }

[复制代码](javascript:void(0)😉

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

webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。使用SpringBoot需要使用webjars,我们可以去搜索一下

网站:https://www.webjars.org/ 【网站带看,并引入jQuery测试】

要使用jQuery,我们只要要引入jQuery对应版本的pom依赖即可!【导入完毕,查看webjars目录结构,并访问Jquery.js文件】

导入依赖:

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

查看目录结构:

img

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

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

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

[复制代码](javascript:void(0)😉

@ConfigurationProperties(
    prefix = "spring.resources",
    ignoreUnknownFields = false
)
public class ResourceProperties {
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};

[复制代码](javascript:void(0)😉

总结:
1.在springboot,我们可以使用以下方式处理静态资源

  • webjars localhost: 8080/webjars/
  • public, static, /**, resources

localhost: 8080/
2.优先级: resources>static>publtc

resourceProperties 可以设置和我们静态资源有关的参数,this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS; 这里面指向了它会去寻找资源的文件夹;即上面数组的内容。再向下看一个方法;它还会在根路径下去寻找资源!

[复制代码](javascript:void(0)😉

    private String[] appendSlashIfNecessary(String[] staticLocations) {
        String[] normalized = new String[staticLocations.length];

        for(int i = 0; i < staticLocations.length; ++i) {
            String location = staticLocations[i];
            normalized[i] = location.endsWith("/") ? location : location + "/";
        }

        return normalized;
    }

[复制代码](javascript:void(0)😉

所以得出结论:

"classpath:/META-INF/resources/", 
"classpath:/resources/",
 "classpath:/static/", 
"classpath:/public/",
"/" :当前项目的根目录

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

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

静态资源文件夹说完后,我们继续看源码!可以看到一个欢迎页的映射,就是我们的首页!

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

点进去继续看

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>首页</h1>
</body>
</html>

我们继续向下阅读源码,可以看到一个好玩的功能,配置我们喜欢的图标!

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

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

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

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

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

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

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

模板引擎

前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。jsp支持非常强大的功能,包括能写Java代码,但是呢,我们现在的这种情况,SpringBoot这个项目首先是以jar的方式,不是war,像第二,我们用的还是嵌入式的Tomcat,所以呢,他现在默认是不支持jsp的。

那不支持jsp,如果我们直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦,那怎么办呢,SpringBoot推荐你可以来使用模板引擎。

那么这模板引擎,我们其实大家听到很多,其实jsp就是一个模板引擎,还有以用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他们的思想都是一样的,什么样一个思想呢我们来看一下这张图。

img

模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些值,从哪来呢,我们来组装一些数据,我们把这些数据找到。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。只不过呢,就是说不同模板引擎之间,他们可能这个语法有点不一样。其他的我就不介绍了,我主要来介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,这模板引擎呢,是一个高级语言的模板引擎,他的这个语法更简单。而且呢,功能更强大。

我们呢,就来看一下这个模板引擎,那既然要看这个模板引擎。首先,我们来看SpringBoot里边怎么用。

第一步:引入thymeleaf , 怎么引入呢,对于springboot来说,什么事情不都是一个start的事情嘛,我们去在项目中引入一下。给大家三个网址:

1、Thymeleaf 官网:https://www.thymeleaf.org/

2、Thymeleaf 在Github 的主页:https://github.com/thymeleaf/thymeleaf

3、Spring官方文档:“https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/htmlsingle/#using-boot-starter” , 找到我们对应的版本

[复制代码](javascript:void(0)😉

        <!--thymeleaf模板,我们都是基于3.x开发-->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-java8time</artifactId>
        </dependency>

[复制代码](javascript:void(0)😉

maven会自动下载jar包,我们可以去看下下载的东西;

img

使用Thymeleaf

前面呢,我们已经引入了Thymeleaf,那这个要怎么使用呢?
我们首先得按照SpringBoot的自动配置原理看一下我们这个Thymeleaf的自动配置规则,在按照那个规则,我们进行使用。
我们去找一下Thymeleaf的自动配置类;

img

这个包下有我们的thymeleaf,看我们的配置类,我们选择部分

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

我们可以在其中看到默认的前缀和后缀!我们只需要把我们的html页面放在类路径下的templates下,thymeleaf就可以帮我们自动渲染了。

我们可以去测试一下 , 写一个Controller,跳转到一个指定页面,这个指定页面需要在 类路径下的模板目录下 【演示】

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

Thymeleaf语法

要学习语法,还是参考官网文档最为准确,我们找到对应的版本看一下;

Thymeleaf 官网:https://www.thymeleaf.org/ , 简单看一下官网!我们去下载Thymeleaf的官方文档!

我们做个最简单的练习 : 我们需要查出一些数据,在页面中展示

我们去在controller编写一个请求,放进去一些数据;

[复制代码](javascript:void(0)😉

@Controller
public class HelloController {
    @RequestMapping("/test")
    public String success(Model model){
        //存入数据
        model.addAttribute("msg","Hello,Thymeleaf");
        //classpath:/templates/success.html
        return "test";
    }
}

[复制代码](javascript:void(0)😉

我们要使用thymeleaf,需要在html文件中导入命名空间的约束,方便提示。我们可以去官方文档的#3中看一下命名空间拿来过来

 xmlns:th="http://www.thymeleaf.org"

我们去编写下前端页面

[复制代码](javascript:void(0)😉

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>狂神说</title>
</head>
<body>
<!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样-->
<!--所有的htmL元素都可以被thymeleaf替换接管: th:元素名-->
<div th:text="${msg}"></div>
</body>
</html>

[复制代码](javascript:void(0)😉

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

一、我们可以使用任意的 th:attr 来替换Html中原生属性的值!【测试】 全部属性可以参考官网文档#10; th语法img

二、我们能写那些表达式呢?我们可以看到官方文档 #4

[复制代码](javascript:void(0)😉

Simple expressions:(表达式语法)
Variable Expressions: ${...}:获取变量值;OGNL;
    1)、获取对象的属性、调用方法
    2)、使用内置的基本对象: #18
         #ctx : the context object.
         #vars: the context variables.
         #locale : the context locale.
         #request : (only in Web Contexts) the HttpServletRequest object.
         #response : (only in Web Contexts) the HttpServletResponse object.
         #session : (only in Web Contexts) the HttpSession object.
         #servletContext : (only in Web Contexts) the ServletContext object.
                
                ${session.foo}
      3)、内置的一些工具对象:
      #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).
==============================================================================================
Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
   补充:配合 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>
    
Message Expressions: #{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL;
            @{/order/process(execId=${execId},execType='FAST')}
Fragment Expressions: ~{...}:片段引用表达式
            <div th:insert="~{commons :: main}">...</div>
            
Literals(字面量)
      Text literals: 'one text' , 'Another one!' ,…
      Number literals: 0 , 34 , 3.0 , 12.3 ,…
      Boolean literals: true , false
      Null literal: null
      Literal tokens: one , sometext , main ,…
Text operations:(文本操作)
    String concatenation: +
    Literal substitutions: |The name is ${name}|
Arithmetic operations:(数学运算)
    Binary operators: + , - , * , / , %
    Minus sign (unary operator): -
Boolean operations:(布尔运算)
    Binary operators: and , or
    Boolean negation (unary operator): ! , not
Comparisons and equality:(比较运算)
    Comparators: > , < , >= , <= ( gt , lt , ge , le )
    Equality operators: == , != ( eq , ne )
Conditional operators:条件运算(三元运算符)
    If-then: (if) ? (then)
    If-then-else: (if) ? (then) : (else)
    Default: (value) ?: (defaultvalue)
Special tokens:
    No-Operation: _ 

[复制代码](javascript:void(0)😉

测试练习

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

前端

[复制代码](javascript:void(0)😉

<!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>

[复制代码](javascript:void(0)😉

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

SpringMVC自动配置

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

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

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

[复制代码](javascript:void(0)😉

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.

[复制代码](javascript:void(0)😉

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

ContentNegotiatingViewResolver

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

 Iterator var5 = this.viewResolvers.iterator();

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

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

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

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

[复制代码](javascript:void(0)😉

package com.kuang.config;

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

import java.util.Locale;

//如果。你想diy- 些定制化的功能。只要写这个组件。然后将它交给springboot, springboot就会帮我们自动装配!
//书展springmvc dispatchservlet
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    //ViewResolver实现视图解析器按口的类,我们就可以把它看做视图解析器
    @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;
        }
    }
}

[复制代码](javascript:void(0)😉

怎么看我们自己写的视图解析器有没有起作用呢?我们给dispatcherServlet中的 doDispatch方法加个断点进行调试一下,因为所有的请求都会走到这个方法中

img

我们启动我们的项目,然后随便访问一个页面,看一下Debug信息;

找到this;

img

找到视图解析器,我们看到我们自己定义的就在这里了;

img

所以说,我们如果想要使用自己定制化的东西,我们只需要给容器中添加这个组件就好了!剩下的事情SpringBoot就会帮我们做了

转换器和格式化器

找到格式化转换器

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

点击去

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

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

我们可以在配置文件中配置日期格式化的规则:

img

img

修改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.

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

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

我们去浏览器访问一下:

img

确实也跳转过来了!所以说,我们要扩展SpringMVC,官方就推荐我们这么去使用,既保SpringBoot留所有的自动配置,也能用我们扩展的配置!

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

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

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

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

  4. [复制代码](javascript:void(0)😉

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

    [复制代码](javascript:void(0)😉

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

    this.configurers.addViewControllers(registry);
    
    

    我们点进去看一下

    [复制代码](javascript:void(0)😉

        public void addViewControllers(ViewControllerRegistry registry) {
            Iterator var2 = this.delegates.iterator();
    
            while(var2.hasNext()) {            //将所有的WebMvcConfigurer相关配置来一起调用!包括我们自己配置的和Spring给我们配置的
                WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
                delegate.addViewControllers(registry);
            }
    
        }
    
    

    [复制代码](javascript:void(0)😉

  5. 所以得出结论:所有的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给我们配置的静态资源映射一定会无效,我们可以去测试一下;

不加注解之前,访问首页

img

给配置类加上注解:@EnableWebMvc

img

我们发现所有的SpringMVC自动配置都失效了!回归到了最初的样子;

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

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

1.这里发现它是导入了一个类,我们可以继续进去看

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

2.它继承了一个父类

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
}

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

[复制代码](javascript:void(0)😉

@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 {
}

[复制代码](javascript:void(0)😉

\4. 总结一句话:@EnableWebMvc将WebMvcConfigurationSupport组件导入进来了;而导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能!

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

RestfulCRUD

准备工作

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

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

pojo实体类

img Department.java

img Employee.java

dao层

img DepartmentDao

img EmployeeDao

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

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

准备工作:OK!!!

首页实现

要求一:默认访问首页

方式一:写一个controller实现!

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

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

解决了这个问题,我们还需要解决一个资源导入的问题;

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

server.servlet.context-path=/kuang

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

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

<link href="asserts/css/bootstrap.min.css" th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">

比如以上这样,无论我们项目名称如何变化,它都可以自动的寻找到!看源代码就可以看出来区别

img

页面国际化

第一步 :编写国际化配置文件,抽取页面需要显示的国际化页面消息。我们可以去登录页面查看一下

先在IDEA中统一设置properties的编码问题!

img

我们在resources资源文件下新建一个i18n目录,建立一个login.propetries文件,还有一个login_zh_CN.properties,发现IDEA自动识别了我们要做国际化操作;文件夹变了

img

我们可以在这上面去新建一个文件;

img

弹出如下页面:我们再添加一个英文的;

img

这样就快捷多了!

img

接下来,我们就来编写配置,我们可以看到idea下面有另外一个视图;

img

这个视图我们点击 + 号就可以直接添加属性了;我们新建一个login.tip,可以看到边上有三个文件框可以输入

img

我们添加一下首页的内容!

img

然后依次添加其他页面内容即可!

img

然后去查看我们的配置文件;

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;

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

我们真实 的情况是放在了i18n目录下,所以我们要去配置这个messages的路径;

spring.messages.basename=i18n.login

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

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

我们去页面测试下;

img

其余同理!IDEA还有提示,非常智能的!

我们可以去打开项目,访问一下,发现已经自动识别为中文的了!

img

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

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

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

AcceptHeaderLocaleResolver 这个类中有一个方法

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

那假如我们现在想点击链接让我们的国际化资源生效,就需要让我们自己的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>

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

[复制代码](javascript:void(0)😉

package com.kuang.myproject.component;

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

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

//可以在链接上携带区域信息
public class 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) {

    }
}

[复制代码](javascript:void(0)😉

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

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

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

登录 + 拦截器

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

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

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

我们把登录页面的表单提交地址写一个controller!

<form class="form-signin" action="dashboard.html" th:action="@{/user/login}" method="post">//这里面的所有表单标签都需要加上一个name属性
</form>

去编写对应的controller

[复制代码](javascript:void(0)😉

package com.kuang.myproject.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class LoginController {

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

        if (!StringUtils.isEmpty(username) && "123456".equals(password)){
            //登录成功!
            return "dashboard";
        }else {
            //登录失败!存放错误信息
            model.addAttribute("msg","用户名密码错误");
            return "index";
        }

    }

}

[复制代码](javascript:void(0)😉

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

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

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

OK ,测试登录成功!如果模板出现500错误,参考处理连接:https://blog.csdn.net/fengzyf/article/details/83341479

img

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

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

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

刷新测试 :

img

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

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

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

将 Controller 的代码改为重定向;

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

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

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

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

我们先自定义一个拦截器

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

在Controller层添加session:

@Controller
public class LoginController {

   @RequestMapping(value = "/user/login")
   //@PostMapping(value="/user/login")
   //@ResponseBody
   public String login(@RequestParam("name") String username,
                       @RequestParam("pwd") String password,
                       Model model, HttpSession session) {
       if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
           //登录成功!
           session.setAttribute("loginUser", username);

           return "redirect:/main.html";
       } else {
           //登录失败!存放错误信息
           model.addAttribute("msg", "用户名或密码错误");
           return "index";
       }

   }

}

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

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

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

自亮设置

list.html

dashboard.html

<div th:replace="~{commons/commons::topbar(active='main.html')}"></div>

commons.html

员工列表功能

要求 : 我们需要使用 Restful风格实现我们的crud操作!

img

看看一些具体的要求,就是我们小实验的架构;

img

我们根据这些要求,来完成第一个功能,就是我们的员工列表功能!

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

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

2.a链接添加请求

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

3.将list放在emp文件夹下

img

4.编写处理请求的controller

[复制代码](javascript:void(0)😉

@Controller
public class EmployeeController {

    @Autowired
    EmployeeDao employeeDao;

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

}

[复制代码](javascript:void(0)😉

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

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

Thymeleaf 公共页面元素抽取

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

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

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

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

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

效果:可以看到已经成功加载过来了!

img

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

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

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

保证这一步做完!

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

为了重用更清晰,我们建立一个commons文件夹,专门存放公共页面;

img

我们去页面中引入一下

<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,动态激活搞定!

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

OK,显示全部员工OK!

添加员工信息

\1. 将添加员工信息改为超链接

<!--添加员工按钮-->
<h2> <a class="btn btn-sm btn-success" href="emp" th:href="@{/emp}">添加员工</a></h2>

2.编写对应的controller

    //to员工添加页面
    @GetMapping("/emp")
    public String toAddPage(){
        return "emp/add";
    }

3.添加前端页面;复制list页面,修改即可

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

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

[复制代码](javascript:void(0)😉

<form th:action="@{/emp}" method="post">
                        <table class="table table-striped sortable">
                            <thead>
                            </thead>
                            <tbody>
                            <tr>
                                <th>LastName</th>
                                <td><input type="text" name="lastName"/><span class="error"/></td>
                            </tr>
                            <tr>
                                <th>Email</th>
                                <td><input type="email" name="email"/></td>
                            </tr>
                            <tr>
                                <th>Gender</th>
                                <td>
                                    <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>
                                </td>
                            </tr>
                            <tr>
                                <th>department</th>
                                <td><select class="form-control" name="department.id">
                                    <option th:each="dept:${departments}" th:text="${dept.departmentName}"
                                            th:value="${dept.id}"></option>
                                </select></td>
                            </tr>
                            <tr>
                                <th>Birth</th>
                                <td><input type="text" name="birth"/></td>
                            </tr>
                            <tr>
                                <td></td>
                                <td><input class="btn btn-success" type="submit"
                                           value="添加"/>&nbsp;&nbsp;&nbsp;<input class="btn btn-danger"
                                                                                type="reset" value="重置"/></td>
                                <td></td>
                                <td></td>
                                <td></td>
                            </tr>
                            </tbody>
                        </table>
                    </form>

[复制代码](javascript:void(0)😉

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

controller

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

前端

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

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

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

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

2.编写controller;

[复制代码](javascript:void(0)😉

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

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

[复制代码](javascript:void(0)😉

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

原理探究 : ThymeleafViewResolver

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

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

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

前端填写数据,注意时间问题

img

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

那我们将时间换一个格式提交

img

提交发现页面出现了400错误!

img

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

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

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

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

调用了 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

[复制代码](javascript:void(0)😉

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

        return "emp/update";
    }

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

测试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就会帮我们自动使用了!

img

注销功能

<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;

聊如何写一个网站

1.前端搞定: 页面长什么样子:数据
2.设计数据库(数据库设计难点! )
3.前端让他能够自动运行,独立化工程
4.数据接口如何对接: json, 对象all in one !
5.前后端联调测试!
1.有一套自己熟悉的后台模板:工作必要! x-admin
2.前端界面:至少自己能够通过前端框架,组合出来-个网站页 面- index - about -
blog- post- user

4、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

我去新建一个项目测试:springboot_demo_data ; 引入相应的模块!基础模块

img

项目建好之后,发现自动帮我们导入了如下的启动器:

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

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

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

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

\3. 配置完这一些东西后,我们就可以直接去使用了,因为SpringBoot已经默认帮我们进行了自动配置;我们去测试类测试一下

[复制代码](javascript:void(0)😉

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

}

[复制代码](javascript:void(0)😉

输出结果:我们可以看到他默认给我们配置的数据源为 : 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方法:用于执行存储过程、函数相关语句。

测试

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

页面访问测试,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 指定自定义的数据源类型,值为 要使用的连接池实现的完全限定名。默认情况下,它是从类路径自动检测的。

[复制代码](javascript:void(0)😉

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

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

[复制代码](javascript:void(0)😉

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

引入数据源

第一步需要在应用的 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>

查看项目依赖,导入成功!

img

现在我们去切换数据源;

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

img

数据源切换之后,同理可以注入 DataSource,然后获取到它,输出一看便知是否成功切换;

img

切换成功!既然切换成功,就可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数 等设置项;

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

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

log4j日志依赖

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

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

[复制代码](javascript:void(0)😉

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

}

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

}

[复制代码](javascript:void(0)😉

输出结果 :可见配置参数已经生效

img

配置 Druid 数据源监控

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

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

[复制代码](javascript:void(0)😉

    //配置 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 中找到
    }

[复制代码](javascript:void(0)😉

配置完毕后,我们可以选择访问 : http://localhost:8080/druid/login.html

img

进入之后

img

配置 Druid web 监控 filter

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

[复制代码](javascript:void(0)😉

    //配置 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;
    }

[复制代码](javascript:void(0)😉

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

我们发送一条sql语句,然后来看一下后台的消息;

img

测试OK!

SpringBoot 整合mybatis

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
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

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

[复制代码](javascript:void(0)😉

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

    }

}

[复制代码](javascript:void(0)😉

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

3,创建实体类

[复制代码](javascript:void(0)😉

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 + '\'' +
                '}';
    }

}

[复制代码](javascript:void(0)😉

4.配置Mapper接口类

[复制代码](javascript:void(0)😉

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 : 表示本类是一个 MyBatis 的 Mapper,等价于以前 Spring 整合 MyBatis 时的 Mapper 接口
@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);

}

[复制代码](javascript:void(0)😉

5.Mapper映射文件

[复制代码](javascript:void(0)😉

<?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>

[复制代码](javascript:void(0)😉

6.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 中查看所有配置项

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

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

7.编写controller

[复制代码](javascript:void(0)😉

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";
    }

}

[复制代码](javascript:void(0)😉

运行测试OK!

Mybatis
整合包
mybatis-spring-boot-starter
1.导入包
2.配置文件

3.mybatis配置
4.编写sql

5.service层调用dao层

6.controller调用service层

5、SpringSecurity (安全)

在web开发中,安全第一位! 过滤器, 拦截器~

功能性需求:否

做网站:安全应该在什么时候考虑?设计之初!

●漏洞,隐私泄露~

●架构一旦确定~

shiro、SpringSecurity: 很像~ 除了类不一样,名字不一样;

认证,授权(vip1, vip2, vip3)

●功能权限
●访问权限
●菜单权限

●... 拦截器,过滤器:大量的原生代码~冗余

MVC- SPRING--SPRINGBOOT---框架思想

Aop :横切~配置类

简介

Spring Security是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大
的Web安全控制,对于安全控制,我们仅需要引入spring-boot-starter-security模块,进行少量的配置,即可实
现强大的安全管理!
记住几个类:
●WebSecurityConfigurerAdapter: 自定义Security策略
●AuthenticationManagerBuilder: 自定义认证策略
●@EnableWebSecurity: 开启WebSecurity模式,@Enablexxx 开启某个功能
Spring Security的两个主要目标是“认证”和“授权”(访问控制)。
“认证”(Authentication)
"授权”(Authorization)
这个概念是通用的,而不是只在Spring Security中存在。
参考官网: https://spring.io/projects/spring-security. 查看我们自己项目中的版本,找到对应的帮助文档:
https://docs. spring.io/spring-security/site/docs/5.2.0.RELEASE/reference/htmlsingle

1、SpringSecurity环境搭建

2、用户认证和授权

//通用
@EnableWebSecurity
pub1ic class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    protected void configure(HttpSecurity http) throws Exception {
    }
}

授权

认证

3、注销及权限控制

注销

权限控制

导入security和thymeleaf整合包

 <!--security- thymeleaf整合包-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity4</artifactId>
            <version>3.0.4.RELEASE</version>
        </dependency>

在前台页面中使用

4、记住我及首页定制

6、Shiro(安全)

6.1、Shiro简介

1.1、什么是Shiro?

●Apache Shiro是一个Java 的安全(权限)框架。
●Shiro可以非常容易的开发出足够好的应用,不仅可以用在JavaSE环境,也可以用在JavaEE环境。
●Shiro可以完成,认证,授权,加密,会话管理,Web集成,缓存等。
●下载地址: http://shiro.apache.org/

1.2、有哪些功能?

●Authentication: 身份认证、登录,验证用户是不是拥有相应的身份;
●Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么操
作,如:验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限!
●Session Manager:会话管理,即用户登录后就是第一次会话,在没有退出之前,它的所有信息都在会话中;
会话可以是普通的avaSE环境,也可以是Web环境;
●Cnyptography: 加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储;
●Web Support: Web支持,可以非常容易的集成到Web环境;
●Caching: 缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高效率
●Concurrency: Shiro支持多线程应用的并发验证,即,如在一个线程中开启另一 个线程,能把权限自动的传
播过去
●Testing:提供测试支持;

●Run As: 允许一 个用户假装为另- -个用户(如果他们允许)的身份进行访问;
●Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了

1.3、Shiro架构(外部)

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

●subject: 应用代码直接交互的对象是Subject, 也就是说Shiro的对外API核心就是Subject, Subject代表了当
前的用户,这个用户不-定是一个具体的人, 与当前应用交互的任何东西都是Subject,如网络爬虫,机器人
等,与Subject的所有交互都会委托给SecurityManager; Subject其实是一 个门面, SecurityManageer 才是
实际的执行者
●SecurityManager: 安全管理器,即所有与安全有关的操作都会与SercurityManager交互,并且它管理着所有
的Subject,可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC的
DispatcherServlet的角色
●Realm: Shiro从Realm获取安全数据 (如用户,色,权限),就是说SecurityManager 要验证用户身份,
那么它需要从Realm获取相应的用户进行比较,来确定用户的身份是否合法;也需要从Realm得到用户相应的
角色、权限,进行验证用户的操作是否能够进行,可以把Realm看成DataSource;

1.4、Shiro架构(内部)

●Subject: 任何可以与应用交互的"用户;
●Security Manager:相当于SpringMVC中的DispatcherServlet;是Shiro的心脏,所有具体的交互都通过
Security Manager进行控制,它管理者所有的Subject,且负责进行认证,授权,会话,及缓存的管理。
●Authenticator: 负责Subject认证,是一一个扩 展点,可以自定义实现;可以使用认证策略(Authentication
Strategy),即什么情况下算用户认证通过了;
●Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中
的那些功能;
●Realm: 可以有一个或者多个的realm,可以认为是安全实体数据源,即用于获取安全实体的,可以用DBC实
现,也可以是内存实现等等,由用户提供;所以- -般在应用中都需要实现自己的realm
●SessionManager: 管理Session生命周期的组件,而Shiro并不仅仅可以用在Web环境,也可以用在普通的
JavaSE环境中
●CacheManager: 缓存控制器,来管理如用户,角色,权限等缓存的;因为这些数据基本上很少改变,放到缓
存中后可以提高访问的性能;
●Cryptography:密码模块, Shiro 提高了一些常见的加密组件用于密码加密, 解密等

6.2、HelloWorld

2.1.快速实践

查看官网文档: http://shiro.apache.org//tutorial.html
官方的quickstart: https://github.com/apache/shiro/blob/master/samples/quickstart
1.创建一-个maven父工程, 用于学习Shiro, 删掉不必要的东西
2.创建一个普通的Maven子工程: shiro-01-helloworld
3.根据官方文档,我们来导入Shiro的依赖

2.1.步骤

1.导入依赖

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

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

2.配置文件

log4j.properties

log4j.rootLogger=INFO, stdout

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

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

# Spring
log4j.logger.org.springframework=WARN

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

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

shiro.ini

#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#
# =============================================================================
# Quickstart INI Realm configuration
#
# For those that might not understand the references in this file, the
# definitions are all based on the classic Mel Brooks' film "Spaceballs". ;)
# =============================================================================

# -----------------------------------------------------------------------------
# Users and their assigned roles
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setUserDefinitions JavaDoc
# -----------------------------------------------------------------------------
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

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

3.hello!word

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


public class Quickstart {

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


    public static void main(String[] args) {


        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);


        // get the currently executing user:
        //获取当前用户对象Subject
        Subject currentUser = SecurityUtils.getSubject();

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

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

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

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

        //粗粒度
        if (currentUser.isPermitted("lightsaber:wield")) {
            log.info("You may use a lightsaber ring.  Use it wisely.");
        } else {
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

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

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

4.测试

6.3、SpringBoot整合Shiro环境搭建

导入相关依赖

<!--shiro整合spring的包-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.1</version>
        </dependency>

        <!--thymeleaf模板,我们都是基于3.x开发-->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-java8time</artifactId>
        </dependency>

测试一个SpringBoot环境是否搭建成功

1

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

</body>
</html>

2

package com.kuang.controller;

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

@Controller
public class MyController {
    @RequestMapping({"/", "index"})
    public String toIndex(Model model) {
        model.addAttribute("msg", "hello,成博");
        return "index";
    }
}

3

测试SpringBoot整合Shiro环境搭建成功

Shiro配置

package com.kuang.config;

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

public class UserRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了=》授权doGetAuthorizationInfo");
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了=》验证doGetAuthorizationInfo");
        return null;
    }
}


ShiroConfig

package com.kuang.config;

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

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

    }

    //Dafaul tWebSecurityManager:2
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultwebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //.关联UserReaLm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    //创建realm对象,需要自定义类:1
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }
}

MyController

package com.kuang.controller;

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

@Controller
public class MyController {
    @RequestMapping({"/", "index"})
    public String toIndex(Model model) {
        model.addAttribute("msg", "hello,成博");
        return "index";
    }

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

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

}

index.html

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

6.4、shiro实现登录拦截

package com.kuang.config;

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

import java.util.LinkedHashMap;
import java.util.Map;

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

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

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


        return bean;

    }

    //Dafaul tWebSecurityManager:2
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultwebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //.关联UserReaLm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    //创建realm对象,需要自定义类:1
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }
}


login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>登录</h1>
<form action="">
    <p>用户名: <input type="text" name="username"></p>
    <p>密码: <input type="text" name="password"></p>
    <p><input type="submit"> </p>
</form>

</body>
</html>

MyController

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

6.5、Shiro实现用户认证

MyController

@RequestMapping("/login")
    public String login(String username, String password, Model model) {
        //获取当前的用户
        Subject subject = SecurityUtils.getSubject();
        //封装用户的登录数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token); //执行登录方法,如果没有异常就说明OK了
            return "index";
        } catch (UnknownAccountException e) { //用户名不存在
            model.addAttribute("msg", "用户名错误");
            return "login";
        } catch (IncorrectCredentialsException e) { //密码不存在
            model.addAttribute("msg", "密码错误");
            return "login";
        }
    }

login.html

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

</body>
</html>

UserRealm

package com.kuang.config;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class UserRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了=》授权doGetAuthorizationInfo");
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=》验证doGetAuthorizationInfo");
        //用户名,密码数据中取
        String name = "root";
        String password = "123456";
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        if (!userToken.getUsername().equals(name)) {
            return null; //抛出异常UnknownAccountException
        }
        //密码认证。shiro 做~
        return new SimpleAuthenticationInfo("", password, "");

    }
}


6.6、Shiro整合Mybatis

1.导入相关依赖

<!--整合mybatis-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.7</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.12</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>


2.连接数据库application.yml

spring:
  datasource:
    username: root
    password: 981204
    #?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

3.其它设置application.properties

mybatis.type-aliases-package=com.kuang.pojo
mybatis.mapper-locations=mapper/*.xml

4.实体类

public class User {
    private int id;
    private String name;
    private String pwd;
    //tostring等
}

5.UserMapper

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

6.UserMapper.xml

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

</mapper>

7.UserService

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

8.UserServiceImpl

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

9.测试下查出对应User

@SpringBootTest
class ShiroSpringbootApplicationTests {
@Autowired
    UserServiceImpl userService;
    @Test
    void contextLoads() {
        System.out.println(userService.queryUserByName("雏田"));
    }
}

10、测试成功后 将数据绑定到安全配置中,再次启动配置即可

package com.kuang.config;

import com.kuang.pojo.User;
import com.kuang.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;

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

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=》验证doGetAuthorizationInfo");
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        //连按真实的数据库
        User user = userService.queryUserByName(userToken.getUsername());
        if (user == null) { //没有这个个
            return null; //UnknownAccountException
        }
        //可以加密: MD5: e10adc3949ba59abbe56e057f20f883e MD5 盐值加密: e10adc3949ba59abbe56e057f20f883eusername
        //密码认证, shiro 做~
        return new SimpleAuthenticationInfo("", user.getPwd(), "");

    }
}

6.7、Shiro请求授权实现

6.7.1.授权,正常的情况下,没有授权会跳转到未授权页面

package com.kuang.config;

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

import java.util.LinkedHashMap;
import java.util.Map;

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

        /*anon:无需认 证就可以访问
        authc:必须认证了 才能让问
        user:.必须拥有 记住我功能才能用
        perms:
        拥有对某个资源的权限才能访间:
        role:拥有某 个角色权限才能访问
        */
        Map<String, String> filterMap = new LinkedHashMap<>();

        //授权,正常的情况下,没有授权会跳转到未授权页面
        filterMap.put("/user/add", "perms[user:add]");
        filterMap.put("/user/update", "perms[user:update]");

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

        //设置获录的请求
        bean.setLoginUrl("/toLogin");
        //未授权页面
        bean. setUnauthorizedUrl("/noauth");


        return bean;

    }

    //Dafaul tWebSecurityManager:2
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultwebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //.关联UserReaLm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    //创建realm对象,需要自定义类:1
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }
}

 @RequestMapping("/noauth")
    @ResponseBody
    public String unauthorized() {
        return "未经授权无法访问此页面";
    }

6.7.2授予可以访问的权限

package com.kuang.config;

import com.kuang.pojo.User;
import com.kuang.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了=》授权doGetAuthorizationInfo");
        //SimpleAuthorizat ionInfo
        //授予可以访问的权限
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addStringPermission("user:add");

        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=》验证doGetAuthorizationInfo");
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        //连按真实的数据库
        User user = userService.queryUserByName(userToken.getUsername());
        if (user == null) { //没有这个个
            return null; //UnknownAccountException
        }
        //可以加密: MD5: e10adc3949ba59abbe56e057f20f883e MD5 盐值加密: e10adc3949ba59abbe56e057f20f883eusername
        //密码认证, shiro 做~
        return new SimpleAuthenticationInfo("", user.getPwd(), "");

    }
}

6.7.3一般在数据库里添加权限的字段,所以我们修改数据库,和实体类

实体类

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

UserRealm实现授权的分配

package com.kuang.config;

import com.kuang.pojo.User;
import com.kuang.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;

public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了=》授权doGetAuthorizationInfo");
        //SimpleAuthorizationInfo
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //info.addStringPermission("user:add");
        //拿到当登录 的这个对象
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User) subject.getPrincipal(); //拿到User对象
        //设置当前用户的权限
        info.addStringPermission(currentUser.getPerms());


        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=》验证doGetAuthorizationInfo");
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        //连按真实的数据库
        User user = userService.queryUserByName(userToken.getUsername());
        if (user == null) { //没有这个个
            return null; //UnknownAccountException
        }
        //可以加密: MD5: e10adc3949ba59abbe56e057f20f883e MD5 盐值加密: e10adc3949ba59abbe56e057f20f883eusername
        //密码认证, shiro 做~
        return new SimpleAuthenticationInfo(user, user.getPwd(), "");


    }
}

6.8、Shiro整合Thymeleaf

实现页面只展示用户拥有的权限

1.导入依赖

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

2.注入bean到ShiroConfigz中

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

        /*anon:无需认 证就可以访问
        authc:必须认证了 才能让问
        user:.必须拥有 记住我功能才能用
        perms:
        拥有对某个资源的权限才能访间:
        role:拥有某 个角色权限才能访问
        */
        Map<String, String> filterMap = new LinkedHashMap<>();

        //授权,正常的情况下,没有授权会跳转到未授权页面
        filterMap.put("/user/add", "perms[user:add]");
        filterMap.put("/user/update", "perms[user:update]");

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

        //设置获录的请求
        bean.setLoginUrl("/toLogin");
        //未授权页面
        bean. setUnauthorizedUrl("/noauth");


        return bean;

    }

    //Dafaul tWebSecurityManager:2
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultwebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //.关联UserReaLm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    //创建realm对象,需要自定义类:1
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }
    //整合ShiroDialect;用来整合shoir thymeleaf
    @Bean
    public ShiroDialect getshiroDialect(){
        return new ShiroDialect();
    }
}

3.使用shirothymeleaf

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

</body>
</html>

4.结果

解决上面还存在登录按钮的问题

UserRealm中添加session

@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=》验证doGetAuthorizationInfo");
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        //连按真实的数据库
        User user = userService.queryUserByName(userToken.getUsername());
        if (user == null) { //没有这个个
            return null; //UnknownAccountException
        }
        //可以加密: MD5: e10adc3949ba59abbe56e057f20f883e MD5 盐值加密: e10adc3949ba59abbe56e057f20f883eusername
        //密码认证, shiro 做~
        Subject currentSubject = SecurityUtils.getSubject();
        Session session = currentSubject.getSession();
        session.setAttribute("loginUser", user);

        return new SimpleAuthenticationInfo(user, user.getPwd(), "");

    }
}

前台做判断

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>首页</h1>
<div th:if="${session.loginUser==null}">
    <a th:href="@{/toLogin}">登录</a>
</div>

<p th:text="${msg}"></p>
<hr>
<div shiro:hasPermission="user:add">
    <a th:href="@{/user/add}">add</a>
</div>
<div shiro:hasPermission="user:update">
    <a th:href="@{/user/update}">update</a>
</div>

</body>
</html>

项目结构

7、任务

1、异步任务

以前:服务器响应时间长,客户端体验不好

模拟代码

service

@Service
public class AsyncService {
    public void hello(){
        try {
            Thread. sleep(3000);
        } catch (InterruptedException e) {
            e. printStackTrace( );
        }
        System. out. println("数据正在处理....");
    }
}

controller

@RestController
public class AsyncController {
    @Autowired
    AsyncService asyncService;

    @RequestMapping("/hello")
    public String hello() {
        asyncService.hello(); //停止三秒,转圈H
        return "OK";
    }
}

现在:前台秒刷新,后台处理中

只需两个注解

service

@Service
public class AsyncService {
    //告诉Spring这是一个异步的方法
    @Async
    public void hello(){
        try {
            Thread. sleep(3000);
        } catch (InterruptedException e) {
            e. printStackTrace( );
        }
        System. out. println("数据正在处理....");
    }
}

Springboot09TestApplication

@EnableAsync //开启异步注解功能
@SpringBootApplication
public class Springboot09TestApplication {

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

}

2、邮件任务(历史遗留问题)

1、导入jar包

<!--邮件发送-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

2、编写配置文件application.properties

spring.mail.username=2458736697@qq.com
spring.mail.password=ralnkaqzxixyecjd
spring.mail.host=smtp.qq.com
#开启加密验证
spring.mail.properties.mail.smtp.ssl.enable=true

3、编写测试类

​ 3.1.普通邮件

  ```java

package com.kuang;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;
@SpringBootTest
class Springboot09TestApplicationTests {

@Autowired
JavaMailSenderImpl mailSender;

@Test
void contextLoads() {
    // 一个简单的邮件~
    SimpleMailMessage mailMessage = new SimpleMailMessage();
    mailMessage.setSubject("小狂神你好呀~");
    mailMessage.setText("谢谢你的狂神说Java系列课程~");
    mailMessage.setTo("2458736697@qq.com");
    mailMessage.setFrom("2458736697@qq.com");
    mailSender.send(mailMessage);
}

}
```

​ 3.2.带附件以及文本编辑的邮件

@Test
    void contextLoads2() throws MessagingException {
        //一个复杂的邮件
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        //组装~
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
        //正文
        helper.setSubject("小狂神你好呀~plus");
        helper.setText("<p style='color:red'>谢谢你的狂神说Java系列课程~</p>", true);
        //附件
        helper.addAttachment("1.jpg", new File("C: \\Users\\Administrator\\Desktop\\1.jpg"));
        helper.setTo("24736743@qq.com");
        helper.setFrom("24736743@qq.com");
        mailSender.send(mimeMessage);
    }

​ 3.3.将其封装成一个方法使用

//封装为一个方法
    public void sendMail(Boolean html, String subject, String text) throws MessagingException {
        //一个复杂的邮件
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        //组装~
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, html);
        //正文
        helper.setSubject(subject);
        helper.setText(text, true);
        //附件
        helper.addAttachment("1.jpg", new File("C: \\Users\\Administrator\\Desktop\\1.jpg"));
        helper.setTo("24736743@qq.com");
        helper.setFrom("24736743@qq.com");
        mailSender.send(mimeMessage);
    }

这简单我咋就测不出来尼,烦死了

3、定时任务表达式

Taskscheduler任务调度者

TaskExecutor任务执行者

@Enablescheduling //开启定时功能的注解

@EnableScheduling//开启定时功能的注解
@SpringBootApplication
public class Springboot09TestApplication {

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

@scheduled() //什么时候执行~

@Service
public class ScheduledService {
    //在一个特定的时间执行这个方法~ Timer
    //cron表达式
    //秒 分 时 日 月 周几~
    /*
    30 15 10 * *  ?   每天10点15分30执行一次
    30 0/5 10,18 * * ?  每天10点和18点, 每隔五分钟执行- -次
    0 15 10 ? * 1-6   每个月的周一-到周六10.15分钟执行一次
    */
    @Scheduled(cron = "0/2 * * * * ?")//每两秒执行一次
    public void hello() {
        System.out.println("hello,你被执行了~");
    }
}

Cron表达式

cron在线生成 http://cron.qqe2.com/

8、SpringBoot分布式:Dubbo+zookeeper

基础知识

分布式理论

什么是分布式系统?

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

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

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

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

Dubbo文档

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

在Dubbo的官网文档有这样一张图

img

单一应用架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。

img

适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。

缺点:

1、性能扩展比较难

2、协同开发问题

3、不利于升级维护

垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。

img

通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。

缺点: 公用模块无法重复利用,开发性的浪费

分布式服务架构

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

img

流动计算架构

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

img

RPC

什么是RPC?

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

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

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

RPC基本原理

img

步骤解析:

img

RPC两个核心模块:通讯,序列化。

Dubbo概念

什么是dubbo?

Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

dubbo官网

1.了解Dubbo的特性

2.查看官方文档

dubbo基本概念

img

服务提供者Provider****):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。

服务消费者Consumer****): 调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

注册中心Registry****):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者

监控中心Monitor****):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心

调用关系说明

l 服务容器负责启动,加载,运行服务提供者。

l 服务提供者在启动时,向注册中心注册自己提供的服务。

l 服务消费者在启动时,向注册中心订阅自己所需的服务。

l 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

l 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

l 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

Dubbo环境搭建

点进dubbo官方文档,推荐我们使用Zookeeper 注册中心

什么是zookeeper呢?可以查看官方文档

window下安装zookeeper

\1. 下载zookeeper :地址, 我们下载3.4.14 , 最新版! 解压zookeeper

\2. 运行/bin/zkServer.cmd ,初次运行会报错,没有zoo.cfg配置文件;

可能遇到问题:闪退 !

解决方案:编辑zkServer.cmd文件末尾添加pause 。这样运行出错就不会退出,会提示错误信息,方便找到原因。

img

img

\3. 修改zoo.cfg配置文件

将conf文件夹下面的zoo_sample.cfg复制一份改名为zoo.cfg即可。

注意几个重要位置:

dataDir=./ 临时数据存储的目录(可写相对路径)

clientPort=2181 zookeeper的端口号

修改完成后再次启动zookeeper

img

4.使用zkCli.cmd测试

ls /:列出zookeeper根下保存的所有节点

[zk: 127.0.0.1:2181(CONNECTED) 4] ls /
[zookeeper]

create –e /kuangshen 123:创建一个kuangshen节点,值为123

img

get /kuangshen:获取/kuangshen节点的值

img

我们再来查看一下节点

img

window下安装dubbo-admin

dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。

但是为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不过这个监控即使不装也不影响使用。

我们这里来安装一下:

1. 下载dubbo-admin

地址 :https://github.com/apache/dubbo-admin/tree/master[

](https://github.com/apache/incubator-dubbo-ops)

2、解压进入目录

修改 dubbo-admin\src\main\resources \application.properties 指定zookeeper地址

[复制代码](javascript:void(0)😉

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

[复制代码](javascript:void(0)😉

3.在项目目录下打包dubbo-admin

mvn clean package -Dmaven.test.skip=true 

第一次打包的过程有点慢,需要耐心等待!直到成功!

img

\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;

登录成功后,查看界面

img

安装完成!

SpringBoot + Dubbo + zookeeper

框架搭建

1. 启动zookeeper !

2. IDEA创建一个空项目;

3.创建一个模块,实现服务提供者:provider-server , 选择web依赖即可

4.项目创建完毕,我们写一个服务,比如卖票的服务;

编写接口

package com.kuang.provider.service;

public interface TicketService {
    public String getTicket();
}

编写实现类

[复制代码](javascript:void(0)😉

package com.kuang.provider.service;

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

[复制代码](javascript:void(0)😉

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

6.项目创建完毕,我们写一个服务,比如用户的服务;

编写service

package com.kuang.consumer.service;

public class UserService {
    //我们需要去拿去注册中心的服务
}

需求:现在我们的用户想使用买票的服务,这要怎么弄呢 ?

服务提供者

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及其依赖包,解决日志冲突,还需要剔除日志依赖;

[复制代码](javascript:void(0)😉

        <!-- 引入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>

[复制代码](javascript:void(0)😉

2. 在springboot配置文件中配置dubbo相关属性!

#当前应用名字
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#扫描指定包下服务
dubbo.scan.base-packages=com.kuang.provider.service

3. 在service的实现类中配置服务注解,发布服务!注意导包问题

[复制代码](javascript:void(0)😉

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》";
    }
}

[复制代码](javascript:void(0)😉

逻辑理解 : 应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它发布在指定的注册中心中!

消费者

1.导入依赖,和之前的依赖一样;

[复制代码](javascript:void(0)😉

        <!--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>

[复制代码](javascript:void(0)😉

2.配置参数

#当前应用名字
dubbo.application.name=consumer-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

3. 本来正常步骤是需要将服务提供者的接口打包,然后用pom文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同;

img

4. 完善消费者的服务类

[复制代码](javascript:void(0)😉

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

}

[复制代码](javascript:void(0)😉

5. 测试类编写;

[复制代码](javascript:void(0)😉

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

    @Autowired
    UserService userService;

    @Test
    public void contextLoads() {

        userService.bugTicket();

    }

}

[复制代码](javascript:void(0)😉

启动测试

1. 开启zookeeper

2. 打开dubbo-admin实现监控【可以不用做】

3. 开启服务者

4. 消费者消费测试,结果:

img

监控中心 :

img

ok , 这就是SpingBoot + dubbo + zookeeper实现分布式开发;

9、聊聊现在和未来

三层架构 + MVC

​ 架构 ---> 解耦

开发框架

	### 1,Spring

​ IOC AOP

​ IOC :控制反转

​ 约泡:
​ 泡温泉,泡茶.....泡友

​ 附近的人,打招呼。加微信,聊天,天天聊,--->约泡

​ 浴场(容器) :温泉, 茶庄,泡友
​ 直接进温泉,就有人和你一起了!

​ 原来我们都是自己一步步操作,现在交给容器了!我们需要什么就去拿就可以了
​ AOP :切面(本质,动态代理)
​ 为了解决什么?不影响业务本来的情况下,实现动态增加功能,大量应用在日志,事务...等等

方面

​ Spring是一个轻量级的J ava开源框架,容器

​ 目的:解决企业开发的复杂性问题

​ Spring是春天,觉得他是春天,也十分复杂,配置文件!

	### 2、SpringBoot

​ SpringBoot并不是新东西,就是Spri ng的升级版!

			新一代JavaEE的开发标准,开箱即用! ->拿过来就可以用! 它自动帮我们配置了非常多的东西,我们拿来即用!

​ 特性:约定大于配置!

随着公司体系越来越大,用户越来越多!

微服务架构--->新架构

​ 模块化,功能化!

​ 用户,支付,签到,娱乐,..... ;

​ 人多余多:一台服务器解决不了;在增加服务器 ! 横向

​ 假设A服务器占用98%资源,B服务器只占用了10% 。一-负载均衡;

​ 将原来的整体项目,分成模块化,用户就是一个单独的项目,签到也是一个单独的项目, 项目和项目之前需要通信,如何通信?

​ 用户非常多,而签到十分少! 给用户多一点服务器,给签到少一点服务器!

微服务架构问题?

分布式架构会遇到的四个核心问题?

  1. 这么多服务,客户端该如何去访问?
  2. 这么多服务,服务之间如何进行通信?
  3. 这么多服务,如何治理呢?
  4. 服务挂了,怎么办?

解决方案:

### 3、SpringCloud,

​ 是一套生态,就是来解决以上分布式架构的4个问题

​ 想使用SpringCloud,必须要掌握SpringBoot,因为SpringCloud是基 于SpringBoot;

  1. Spring Cloud NetFlix ,出来了一套解决方案! - -站式解决方案。我们都可以直接去这里拿?

    ​ Api网关,zuul组件

    ​ Feign --> HttpClient ---> HTTP的通信方式,同步并阻塞

    ​ 服务注册与发现,Eureka

    ​ 熔断机制,Hystrix

    ​ 2018年年底,NetFlix宣布无限期停止维护。生态不再维护,就会脱节。

  2. Apache Dubbo zookeeper,第二套解决系统

    ​ API : 没有! 要么找第三方组件,要么自己实现

    ​ Dubbo是一个高性能的基于Java实现的RPC通信框架! 2.6.x

    ​ 服务注册与发现,zookeeper: 动物园管理者(Hadoop ,Hive)

    ​ 没有: 借助了Hystrix

    ​ 不完善,Dubbo .

  3. SpringCloud Alibab一-站式解决方案 !

目前,又提出了一种方案:

​ 服务网格:下一代微服务标准,Service Mesh

​ 代表解决方案: istio (你们未来可能需要掌握! )

万变不离其宗,一通百通!

  1. API网关,服务路由

  2. HTTP,RPC框架,异步调用

  3. 服务注册与发现,高可用

  4. 熔断机制,服务降级

为什么要解决这个问题?

本质:网络是不可靠的!

100、通用

1、常用注解

@SpringBootApplication 来标注一个主程序类 , 说明这是一个Spring Boot应用
@Component 实体类调用springboot的组件
@ConfigurationProperties作用:将配置文件中配置的每一个属性的值,映射到这个组件中;告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应


@PropertySource(value =* classpath;qinjiang. properties" )//加载指定的配置文件
@Value("${name}")//配合EL表达式取出配置文件properties中的值

@Validated  //数据校验,一些格式限定等里面有很多注解如:@Email //name必须是邮箱格式

@Configuration可以添加配置,mvc扩展配置,代表配置到springboot配置中
@EnableWebMvc将WebMvcConfigurationSupport组件导入进来了;而导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能!

@Bean

//@Mapper : 表示本类是一个 MyBatis 的 Mapper,等价于以前 Spring 整合 MyBatis 时的 Mapper 接口
@MapperScan("cn. luischen. dao")和@Mapper作用相同    
                
@EnableSwagger2 //开启Swagger2  
                
@Service
@RestController
@Autowired
@Async //告诉Spring这是一个异步的方法 
@EnableAsync //开启异步注解功能 
@EnableScheduling//开启定时功能的注解                

@Service //将服务发布出去
@Component //放在容器中
@Service //注入到容器中                
@Reference //远程引用指定的服务,他会按照全类名进行匹配,看谁给注册中心注册了这个全类名                

2、通用依赖

 		<!--自研的-->
		<!--log4j-->
        <!-- https://mvnrepository.com/artifact/log4j/log4j -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <!--druid数据源-->
        <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>
  <!-- 引入 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>
<!--thymeleaf模板,我们都是基于3.x开发-->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-java8time</artifactId>
        </dependency>

		 <!--官方的-->
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

		<!--security安全-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
		<!--security- thymeleaf整合包-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity4</artifactId>
            <version>3.0.4.RELEASE</version>
        </dependency>


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

        <!-- configure logging -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.21</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.21</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
    </dependencies>
<!--shiro整合spring的包-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.1</version>
        </dependency>
<!--thymeleaf-shiro整合-->
    <dependencies>
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>



<!--邮件发送-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>


        <!--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>

posted on 2020-03-31 22:44  月佩半琪  阅读(605)  评论(1编辑  收藏  举报