SpringBoot(狂神)

微服务阶段

JavaSE:OOP

mysql:持久化

html + css + jquery + 框架:视图,然而我们做出来的不好原因是框架不熟练,css不好!

JavaWeb:独立开发MVC三层架构的网站:原始!

SSM:框架,简化了我们的开发流程,配置也开始较为复杂!

war:tomcat运行

Spring再简化:SpringBoot - jar:内嵌Tomcat,微服务架构!

服务端越来越多:SpringCloud!

SpringBoot

回顾什么是Spring

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

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

Spring是如何简化开发的

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

1、基于POJO的轻量级和最小侵入性编程,所有东西都是bean;

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

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

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

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

真的很爽,我们快速去体验开发个接口的感觉吧!

微服务

什么是微服务?

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

单体应用架构

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

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

  • 这样做的好处是,易于开发和测试;也十分方便部署;当需要扩展时,只需要将war复制多份,然后放到多个服务器上,再做个负载均衡就可以了。

  • 单体应用架构的缺点是,哪怕我要修改一个非常小的地方,我都需要停掉整个服务,重新打包、部署这个应用war包。特别是对于一个大型应用,我们不可能吧所有内容都放在一个应用里面, 我们如何维护、如何分工合作都是问题。

微服务架构

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

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

这样做的好处是:

  1. 节省了调用资源。

  2. 每个功能元素的服务都是一个 可替换的、可独立升级的软件代码。

 

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

 

如何构建微服务

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

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

  • 构建一个个功能独立的微服务应用单元,可以使用springboot, 可以帮我们快速构建一 个应用

  • 大型分布式网络服务的调用,这部分由spring cloud来完成,实现分布式;

  • 在分布式中间,进行流式数据计算、批处理,我们有spring cloud data flow.

  • spring为我们想清楚了整个从开始构建应用到大型分布式应用全流程方案。

如果连接不到springboot官网:尝试使用:http://start.spring.io/  |  https://start.spring.io/ |  请设置连接:https://start.aliyun.com ,详情参考:https://blog.csdn.net/weixin_44441009/article/details/118357050

1、第一个SpringBoot程序

准备工作

我们将学习如何快速的创建一个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 Initializr 的 Web页面创建项目

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

  2. 填写项目信息

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

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

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

 

项目创建方式二:

使用 IDEA 直接创建项目

  1. 创建一个新项目

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

  3. 填写项目信息

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

  5. 填写项目路径

  6. 等待项目构建成功

 

项目结构分析:

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

1、程序的主启动类

2、一个 application.properties 配置文件

3、一个 测试类

4、一个 pom.xml

 

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.7.0-SNAPSHOT</version>
 <relativePath/> <!-- lookup parent from repository -->
 </parent>
 <groupId>com.kuang</groupId>
 <artifactId>helloword</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <name>helloword</name>
 <description>Demo project for Spring Boot</description>
 <properties>
 <java.version>1.8</java.version>
 </properties>
 <dependencies>
 <!--web依赖: tomcat、dispatcherServlet、xml-->
 <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>
 </dependency>
 </dependencies>
 ​
 <!--spring-boot-start 所有的springboot依赖都是使用这个开头的-->
 ​
 <build>
 <!--打jar包插件-->
 <plugins>
 <plugin>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-maven-plugin</artifactId>
 </plugin>
 </plugins>
 </build>
 <repositories>
 <repository>
 <id>spring-milestones</id>
 <name>Spring Milestones</name>
 <url>https://repo.spring.io/milestone</url>
 <snapshots>
 <enabled>false</enabled>
 </snapshots>
 </repository>
 <repository>
 <id>spring-snapshots</id>
 <name>Spring Snapshots</name>
 <url>https://repo.spring.io/snapshot</url>
 <releases>
 <enabled>false</enabled>
 </releases>
 </repository>
 </repositories>
 <pluginRepositories>
 <pluginRepository>
 <id>spring-milestones</id>
 <name>Spring Milestones</name>
 <url>https://repo.spring.io/milestone</url>
 <snapshots>
 <enabled>false</enabled>
 </snapshots>
 </pluginRepository>
 <pluginRepository>
 <id>spring-snapshots</id>
 <name>Spring Snapshots</name>
 <url>https://repo.spring.io/snapshot</url>
 <releases>
 <enabled>false</enabled>
 </releases>
 </pluginRepository>
 </pluginRepositories>
 ​
 </project>

 

编写一个http接口

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

2、在包中新建一个HelloController类

 @RestController
 public class HelloController {
     // 接口:http://localhost:8080/hello
     @RequestMapping("/hello")
     public String hello(){
         // 调用业务,接收前端参数
         return "hello,word";
    }
   
 }

3、编写完毕后,从主程序启动项目,浏览器发起请求,看页面返回;控制台输出了 Tomcat 访问的端口号!

 

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

将项目打成jar包,点击maven的 package

 

如果遇到以上错误,可以配置打包时 跳过项目运行测试用例

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

如果打包成功,则会在target目录下生成一个 jar 包

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

例:在终端或者cmd管理员运行

  • 终端:java -jar ./helloword-0.0.1-SNAPSHOT.jar

  • 管理员:java -jar .\helloword-0.0.1-SNAPSHOT.jar

 

小彩蛋

如何更改启动时显示的字符拼成的字母,SpringBoot呢?也就是 banner 图案;

只需一步:到项目下的 resources 目录下新建一个banner.txt 即可。

图案可以到:https://www.bootschool.net/ascii 这个网站生成,然后拷贝到文件中即可!

在SpringBoot中如果想要进行更换版本,那么请进行如下配置:

    <properties>
        <mysql.version>8.0.27</mysql.version>
    </properties>

底层注解

1、@Configuration注解

  • Full模式与Lite模式

    • 配置类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断!

    • 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式

 /**
  * 1.配置类里面使用的@bean标注在方法上给容器注册组件,默认也是单实例的!
  * 2、配置类本身也是组件
  * 3.proxyBeanMethods :代理bean的方法
  *      Full (proxyBeanMethods = true) 【保证每个@Bean方法被调用多少次返回的组件都是单实例的】
  *      Lite (proxyBeanMethods = false)【每个@Bean方法被调用多少次返回的都是新创建的】
  *   组件依赖必须使用Full模式默认,其他默认使用Lite模式
  */
 ​
 @Configuration(proxyBeanMethods = true) //告诉springboot这是一个配置类 == 配置文件
 public class MyConfig {
 ​
     @Bean //给容器添加组件,方法名就是id,返回值类型就是class组件类型,返回值就是组件在容器中的实例!
     public User user1(){
         User user = new User("zhansgan", 18);
         user.setPet(tom());//这里user组件依赖Pet组件!
         return user;
     }
 ​
     @Bean
     public Pet tom(){
         return new Pet("tom");
     }
 }
 ​
 ==================@Configuration测试代码如下================
 @SpringBootApplication
 public class MainApplication {
 ​
     public static void main(String[] args) {
         // 1、返回IOC容器
         ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
         // 2、查看容器内的组件!
         String[] names = run.getBeanDefinitionNames();
         for (String name : names) {
             System.out.println(name);
         }
         Pet tom01 = run.getBean("tom", Pet.class);
         Pet tom02 = run.getBean("tom", Pet.class);
         System.out.println("组件:"+(tom01 == tom02));
 ​
         MyConfig bean = run.getBean(MyConfig.class);
         System.out.println(bean);
 ​
         User user1 = run.getBean("user1", User.class);
         Pet pet = run.getBean("tom", Pet.class);
         System.out.println("用户的宠物:"+(user1.getPet()==pet));
     }
 }

2、@Bean、@Component、@Controller、@Repoistory、@Service

这些注解之前已经说过了,所以这里不做介绍了!

3、@ComponentScan、@Import

  • @ComponentScan:扫描包

  • @Import :导入,默认的组件名就是全类名!

4、@Conditional

条件装配:满足@Conditional指定的条件,则进行组件注入!

5、@ImportResource

这个注解看名字导入资源,因为很多公司还是使用xml的方式去开发,那么你在创建beans.xml之后在你想要使用的类上假如这个注解进行绑定即可不影响他之前的东西:

 @ImportResource("classpath:beans.xml")

6、ConfigurationProperties

前提,只有在容器中的组件,才会拥有SpringBoot的提供的功能!

假设我们要给一个实例类赋值,可以直接在配置文件中进行绑定:

  1. :@Component + @ConfigurationProperties

 @Component
 @configurationProperties(prefix= "xxx") //xxx是配置属性的前缀
  1. :@EnableConfigurationProperties + @ ConifgurationProperties

  @ConifgurationProperties(prefix= "xxx")
  @EnableConfigurationProperties(User.class)

这里的注意点 @EnableConfigurationProperties 这个注解是加在配置类上的!

 

2、运行原理初探

pom.xml

2.1、父依赖

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

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

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

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

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

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

 

2.2、启动器 spring-boot-starter

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

springboot-boot-starter-xxx:就是spring-boot的场景启动器

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

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

 

2.3、主启动类

分析完了 pom.xml 来看看这个启动类

1、默认的主启动类

 //SpringBootApplication:标注这是一个 springboot 的应用
 @SpringBootApplication
 public class Springboot01HellowordApplication {
 ​
     public static void main(String[] args) {
         /* 将springboot 应用启动 */
         SpringApplication.run(Springboot01HellowordApplication.class, args);
    }
 ​
 }

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

2、SpringBootApplication

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

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

 @SpringBootConfiguration
 @EnableAutoConfiguration
 @ComponentScan(
     excludeFilters = {@Filter(
     type = FilterType.CUSTOM,
     classes = {TypeExcludeFilter.class}
 ), @Filter(
     type = FilterType.CUSTOM,
     classes = {AutoConfigurationExcludeFilter.class}
 )}
 )
 public @interface SpringBootApplication {
     // ......
 }

@ComponentScan

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

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

@SpringBootConfiguration

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

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

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

@Component
public @interface Configuration {}

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

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

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

@EnableAutoConfiguration

@EnableAutoConfiguration :开启自动配置功能

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

点进注解接续查看:

@AutoConfigurationPackage :自动配置包

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

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

Registrar.class 作用:将主启动类的所在包及包下面所有子包里面的所有组件扫描到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,全局搜索它

spring.factories

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

 

WebMvcAutoConfiguration

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

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

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

结论:

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

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

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

  3. 整个J2EE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;

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

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

现在大家应该大概的了解了下,SpringBoot的运行原理,后面我们还会深化一次!

 

原理分析,图示:

3、SpringApplication

不简单的方法

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

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

SpringApplication.run分析

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

SpringApplication

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

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

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

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

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

查看构造器:

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

run方法流程分析

 

3、Spring Boot 配置

3.1、yaml配置注入

配置文件

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

  • application.properties

    • 语法结构 :key=value

  • application.yml

    • 语法结构 :key:空格 value

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

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

server.port=8081

yaml概述

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

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

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

传统xml配置:

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

yaml配置:

server:
  prot: 8080

yaml基础语法

说明:语法要求严格!

1、空格不能省略

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

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

 

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

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

k: v

注意:

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

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

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

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

对象、Map(键值对)

 #对象、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: 8082

3.2、注入配置文件

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

yaml注入配置文件

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

2、编写一个实体类 Dog;

package com.kuang.springboot.pojo;

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

3、思考,我们原来是如何给bean注入属性值的!@Value,给狗狗类测试一下:

@Component //注册bean
public class Dog {
    @Value("阿黄")
    private String name;
    @Value("18")
    private Integer age;
}

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

@SpringBootTest
class DemoApplicationTests {

    @Autowired //将狗狗自动注入进来
    Dog dog;

    @Test
    public void contextLoads() {
        System.out.println(dog); //打印看下狗狗对象
    }

}

结果成功输出,@Value注入成功,这是我们原来的办法对吧。

 

5、我们在编写一个复杂一点的实体类:Person 类

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

6、我们来使用yaml配置的方式进行注入,大家写的时候注意区别和优势,我们编写一个yaml配置!

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

7、我们刚才已经把person这个对象的所有值都写好了,我们现在来注入到我们的类中!

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

8、IDEA 提示,springboot配置注解处理器没有找到,让我们看文档,我们可以查看文档,找到一个依赖!

 

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

9、确认以上配置都OK之后,我们去测试类中测试一下:

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    Person person; //将person自动注入进来

    @Test
    public void contextLoads() {
        System.out.println(person); //打印person信息
    }

}

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

 

yaml配置注入到实体类完全OK!

课堂测试:

1、将配置文件的key 值 和 属性的值设置为不一样,则结果输出为null,注入失败

2、在配置一个person2,然后将 @ConfigurationProperties(prefix = "person2") 指向我们的person2;

加载指定的配置文件

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

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

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

name=kuangshen

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

@Component  //注册bean
//javaConfig 绑定我们配置文件的值,可以采用下面这种方式!
//加载指定的配置文件
@PropertySource(value = "classpath:lynn.properties")
public class Person {

    //SPEL 表达式取出配置文件的值
    @Value("${name}")
    private String name;
    ......  
}

3、再次输出测试一下:指定配置文件绑定成功!

 

配置文件占位符

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

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

回顾properties配置

我们上面采用的yaml方法都是最简单的方式,开发中最常用的;也是springboot所推荐的!那我们来唠唠其他的实现方式,道理都是相同的;写还是那样写;配置文件除了yml还有我们之前常用的properties , 我们没有讲,我们来唠唠!

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

settings-->FileEncodings 中配置;

 

测试:

1、新建一个实体类User

@Component //注册bean
public class User {
    private String name;
    private int age;
    private String sex;
}

2、编辑配置文件 user.properties

user1.name=kuangshen
user1.age=18
user1.sex=男

3、我们在User类上使用@Value来进行注入!

@Component //注册bean
@PropertySource(value = "classpath:user.properties")
public class User {
    //直接使用@value
    @Value("${user.name}") //从配置文件中取值
    private String name;
    @Value("#{9*2}")  // #{SPEL} Spring表达式
    private int age;
    @Value("男")  // 字面量
    private String sex;
}

4、Springboot测试

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    User user;

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

}

结果正常输出!

 

3.3、对比小结

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

 

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

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

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

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

结论:

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

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

  • 如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!

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

4.1、JSR303 数据校验

先看看如何使用

//SPEL 表达式取出配置文件的值
//@Value("${name}")
@Component //注册bean
@ConfigurationProperties(prefix = "person")
@Validated  //数据校验
public class Person {

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

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

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

常见参数

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

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

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

.......等等
除此以外,我们还可以自定义一些数据校验规则

在这里有一个注意点:如果你发现你的springboot中JSR303校验没有生效,那么这个原因不是因为其他,只是因为SpringBoot在2.3.0.RELEASE版本之后,就要手动导入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.3.3.RELEASE</version>
</dependency>

4.2、多环境切换

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

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

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


---

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

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

配置文件加载位置

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

默认的查找路径如下:

  • file:./config/

  • file:./

  • classpath:/config/

  • classpath:/

 

application.properties:官方配置:https://docs.spring.io/spring-boot/docs/1.5.9.RELEASE/reference/htmlsingle/#common-application-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

5、自动配置原理

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

SpringBoot官方文档中有大量的配置,我们无法全部记住

官方文档:https://docs.spring.io/spring-boot/docs/1.5.9.RELEASE/reference/htmlsingle/#common-application-properties

 

分析自动配置原理

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

这就是自动装配的原理!

精髓

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

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

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

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

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

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

了解:Conditional派生注解

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

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

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

 

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

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

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

#开启springboot的调试类
debug=true

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

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

Unconditional classes: (没有条件的类)

【演示:查看输出的日志】

掌握吸收理解原理,即可以不变应万变!

 

6、SpringBoot Web开发

6.1、简介

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

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

使用SpringBoot的步骤:

1、创建一个SpringBoot应用,选择我们需要的模块,SpringBoot就会默认将我们的需要的模块自动配置好

2、手动在配置文件中配置部分配置项目就可以运行起来了

3、专注编写业务代码,不需要考虑以前那样一大堆的配置了。

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

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

  • 向容器中自动配置组件 :*** Autoconfiguration

  • 自动配置类,封装配置文件的内容:***Properties

没事就找找类,看看自动装配原理!

我们之后来进行一个单体项目的小项目测试,让大家能够快速上手开发!

在web中,我们要解决的问题:

  • 导入静态资源问题…

  • 首页

  • jsp,模板引擎 Thymeleaf

  • 装配扩展SpringMVC

  • 增删改查

  • 拦截器

  • 国际化!

6.2、静态资源处理

静态资源映射规则

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

写请求非常简单,那我们要引入我们前端资源,我们项目中有许多的静态资源,比如css,js等文件,这个SpringBoot怎么处理呢?

如果我们是一个web应用,我们的main下会有一个webapp,我们以前都是将所有的页面导在这里面的,对吧!但是我们现在的pom呢,打包方式是为jar的方式,那么这种方式SpringBoot能不能来给我们写页面呢?当然是可以的,但是SpringBoot对于静态资源放置的位置,是有规定的!

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

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

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

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

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

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

什么是webjars呢?

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

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

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

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

<dependency>
    <groupId>org.webjars.npm</groupId>
    <artifactId>github-com-jquery-jquery</artifactId>
    <version>3.4.1</version>
</dependency>

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

 

访问:只要是静态资源,SpringBoot就会去对应的路径寻找资源,我们这里访问:

http://localhost:8080/webjars/github-com-jquery-jquery/3.4.1/jquery.js

 

第二种静态资源映射规则

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

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

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

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

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

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

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

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

自定义静态资源路径

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

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

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

6.3、首页处理

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

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

点进去继续看

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

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

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

新建一个 index.html ,在我们上面的3个目录中任意一个;然后访问测试 http://localhost:8080/ 看结果!

关于网站图标说明

与其他静态资源一样,Spring Boot在配置的静态内容位置中查找 favicon.ico。如果存在这样的文件,它将自动用作应用程序的favicon。

1、关闭SpringBoot默认图标

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

2、自己放一个图标在静态资源目录下,我放在 public 目录下

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

6.4、静态资源配置原理

1、原理分析

  • springboot启动默认加载 xxxxAutoConfiguration 类(自动配置类)

  • springmvc功能的自动配置类 WebMVCAutoConfiguration ,生效:

 @Configuration(proxyBeanMethods = false)
 @ConditionalOnWebApplication(type = Type.SERVLET)
 @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
 @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
 @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
     ValidationAutoConfiguration.class })
 public class WebMvcAutoConfiguration {}
  • 那么在容器中配置了什么:

   @Configuration(proxyBeanMethods = false)
   @Import(EnableWebMvcConfiguration.class)
   @EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
   @Order(0)
   public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {}
  • 配置文件的相关属性和xxx进行了绑定:WebMvcProperties 、WebProperties

 

配置类只有一个有参构造器,有参构造器的值都会从容器中确定进行查找!

 // WebProperties webProperties  获取和spring.resources绑定的值的对象
 // WebMvcProperties mvcProperties  获取和spring.mvc绑定的值的对象
 // ListableBeanFactory beanFactory  Spring的 beanFactory
 // HttpMessageConverters  找到所有的HttpMessageConverters
 // ResourceHandlerRegistrationCustomizer  找到资源处理器的自定义器
 // DispatcherServletPath  
 // ServletRegistrationBean  给应用注册Servlet、Filter...
 public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties,
       ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
       ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
       ObjectProvider<DispatcherServletPath> dispatcherServletPath,
       ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
    this.resourceProperties = webProperties.getResources();
    this.mvcProperties = mvcProperties;
    this.beanFactory = beanFactory;
    this.messageConvertersProvider = messageConvertersProvider;
    this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
    this.dispatcherServletPath = dispatcherServletPath;
    this.servletRegistrations = servletRegistrations;
    this.mvcProperties.checkConfiguration();
 }

2、资源映射处理规则

     @Override
     public void addResourceHandlers(ResourceHandlerRegistry registry) {
       if (!this.resourceProperties.isAddMappings()) {
         logger.debug("Default resource handling disabled");
         return;
       }
       addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
       addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
         registration.addResourceLocations(this.resourceProperties.getStaticLocations());
         if (this.servletContext != null) {
           ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
           registration.addResourceLocations(resource);
         }
       });
     }
 ​
     private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
       addResourceHandler(registry, pattern, (registration) -> registration.addResourceLocations(locations));
     }
 ​
     private void addResourceHandler(ResourceHandlerRegistry registry, String pattern,
         Consumer<ResourceHandlerRegistration> customizer) {
       if (registry.hasMappingForPattern(pattern)) {
         return;
       }
       ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);
       customizer.accept(registration);
       registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
       registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
       registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
       customizeResourceHandlerRegistration(registration);
     }
 spring:
   web:
     resources:
       add-mappings: false  # 禁用所有静态资源规则,默认为true!
 public static class Resources {
 ​
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
          "classpath:/resources/", "classpath:/static/", "classpath:/public/" };
 ​
    /**
     * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
     * /resources/, /static/, /public/].
     */
    private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
 }

3、欢迎页的处理规则

 @Bean //HandlerMapping:处理器映射,判断每一个Handler能处理哪些请求!
 public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
       FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
          new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
          this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
    return welcomePageHandlerMapping;
 }
 --------------------------------------------------
   WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
       ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
     if (welcomePage != null && "/**".equals(staticPathPattern)) {
       // 要用欢迎页功能,必须是/**
       logger.info("Adding welcome page: " + welcomePage);
       setRootViewName("forward:index.html");
     }
     else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
       // 调用Controller  /index
       logger.info("Adding welcome page template: index");
       setRootViewName("index");
     }
   }

 

6.5、请求参数处理

如果我们想要在提交表单的时候进行Restful风格进行,那么请进行如下操作:

首先看源码:

 @Bean
 @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
 @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled")
 public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new OrderedHiddenHttpMethodFilter();
 }
 -------------------------------
     private static final List<String> ALLOWED_METHODS =
       Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
           HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
 ​
   /** Default method parameter: {@code _method}. */
   public static final String DEFAULT_METHOD_PARAM = "_method";
 ​
   private String methodParam = DEFAULT_METHOD_PARAM;
 --------------------------------
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
       throws ServletException, IOException {
 ​
     HttpServletRequest requestToUse = request;
 ​
     if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
       String paramValue = request.getParameter(this.methodParam);
       if (StringUtils.hasLength(paramValue)) {
         String method = paramValue.toUpperCase(Locale.ENGLISH);
         if (ALLOWED_METHODS.contains(method)) {
           requestToUse = new HttpMethodRequestWrapper(request, method);
         }
       }
     }
 ​
     filterChain.doFilter(requestToUse, response);
   }

在这里我们可以看出表单的提交只能通过Get / Post 的方式进行提交,所以我们需要进行配置,需要在表单操作中,添加name为 _method 的属性,而且 必须是post进行添加!

 <form action="/user" method="post">
     <input type="hidden" name="_method" value="DELETE">
 </form>

然后编写Controller,进行测试:

 @RequestMapping(value = "/user",method = RequestMethod.DELETE)
 public User user(){
     return new User();
 }

Rest原理 (表单提交要使用REST的时候)

  • 表单提交要携带 _method=PUT

  • 请求过来被 HiddenHttpMethodFilter 拦截

    • 请求是否正常,并且是POST

      • 获取到 _method 的值

      • 兼容以下请求:PUT、DELETE、PATCH

      • 原生request (post),包装成requestWrapper重写getMethod方法,返回的是传入的值!

      • 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requestWrapper的

Rest使用客户端工具:

  • 如果是使用postman进行测试,发送各种请求Put、Delete…方式进行访问,无需Filter!

 spring:
   mvc:
     hiddenmethod:
       filter:
         enabled: true  # 开启表单的Rest风格! 

当然在这里肯定有人会想这里的 _method可以进行修改吗?这里当然是可以的,看下面的操作:

首先进行配置类的方法重写,编写一个配置类:

 @Configuration(proxyBeanMethods = false)
 public class WebConfig {
 ​
     @Bean
     public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
         HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
         methodFilter.setMethodParam("_Jk");
         return methodFilter;
     }
 }

然后我们在表单中的方法也需要进行修改:

 <form action="/user" method="post">
     <input type="hidden" name="_Jk" value="DELETE">
 </form>

6.6、请求映射原理

HtppServlet 调用 doGet ––> HttpServletBean 通过 processRequest –> doService ——>DispatcherServlet 通过doService 实现—>doDispath(每个请求都会调用)!

springmvc功能分析通过从 DispatcherServlet—>doDispatch()

 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
 ​
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
 ​
    try {
       ModelAndView mv = null;
       Exception dispatchException = null;
 ​
       try {
          processedRequest = checkMultipart(request);
          multipartRequestParsed = (processedRequest != request);
 ​
          // 找到当前请求使用哪个Handler (Controlelr)处理
          mappedHandler = getHandler(processedRequest);
          if (mappedHandler == null) {
             noHandlerFound(processedRequest, response);
             return;
          }//HandlerMapping 处理器映射! /xxx-->xxx
 }

RequestMappingHandlerMapping:保存了所有的@RequestMapping 和 handler的映射规则!

所有的请求映射都是在HandlerMapping中。

  • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。 访问 / 能访问到index.html。

  • springboot自动配置了默认的 RequestMappingHandlerMapping

  • 请求进来,挨个记性所有的HandlerMapping看是否有请求信息。

    • 如果没有找到这个请求对应的Handler

    • 如果没有就是下一个 HandlerMapping

  • 我们需要一些自定义的映射处理,我们也可以给容器中放HandlerMapping,自定义 HandlerMapping !

6.7、普通参数与基本注解

  • 注解

@PathVariable、@RequestHeader、@ModelAtibute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody

  1. @PathVariable:路径变量

  2. @RequestHeader:获得请求头

  3. @RequestParam:获得请求参数

  4. @CookieValue:获得cookie值

  5. @RequestBody:获得请求体:[POST]

  6. @MatrixVariable:矩阵变量

  7. @RequestAttribute:获得请求域中的值【Request】

  • Servlet API:

WebRequest、ServletRequest、MultipartRequest、HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Local、TimZone、Zoneld

  • 复杂参数:

Map、Errors / BindingResult、Model、RedirectAttributes、ServletResponse、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder

  • 自定义对象参数:

可以自动类型转换与格式化,可以级联封装!

 

应用场景:

@MatrixVariable:矩阵变量

这里我们讲解一下:如果在实际开发中我们页面开发cookie被禁用了,session中的值怎么使用!

原本的开发过程:seeeion.set(a,b) - - >sessionid- -> cookie - - > 每次请求携带!

矩阵变量是通过path进行携带,所以解决方案可以通过url重写:/abc;sessionid=xxx 把session的值通过矩阵变量的方式进行传递!

 <a href="/abc/a=1;age=3,hooby=book,code,music"></a>

然后在controller进行测试,发现访问的地址400错误,没有找到!

     // 在这里SpringBoot默认是禁用了矩阵变量的功能,如果要使用,需要手动换开启!
     //原理;对于路径的处理。UrlPathHelper 进行解析,removeSemicolonContent(移除分号内容) 支持矩阵变量的!
     // 矩阵变量必须有url的路径变量才能被解析!
     @GetMapping("/abc")
     public Map value(@MatrixVariable("id")Integer id, @MatrixVariable("age")Integer age,
                      @MatrixVariable("hobby")List<String> hobby,
                      @PathVariable("path")String path){
         HashMap<String, Object> map = new HashMap<>();
         map.put("id",id);
         map.put("age",age);
         map.put("hobby",hobby);
         map.put("path",path);
         return map;
     }

那么我们就需要自己定义重写方法,从而解决这个问题!

实现方法一:

 @Configuration(proxyBeanMethods = false)
 public class WebConfig implements WebMvcConfigurer {
 ​
     @Override
     public void configurePathMatch(PathMatchConfigurer configurer) {
         UrlPathHelper urlPathHelper = new UrlPathHelper();
         urlPathHelper.setRemoveSemicolonContent(false);
         configurer.setUrlPathHelper(urlPathHelper);
     }
 }

实现方法二:

 @Bean
 public WebMvcConfigurer webMvcConfigurer(){
 ​
     return  new WebMvcConfigurer() {
         @Override
         public void configurePathMatch(PathMatchConfigurer configurer) {
             UrlPathHelper urlPathHelper = new UrlPathHelper();
             urlPathHelper.setRemoveSemicolonContent(false);
             configurer.setUrlPathHelper(urlPathHelper);
         }
     };
 }

那么接下来如果我们遇到了一个问题,就是我们想要获取的值不一样,第一个是bossID,第二个是EmpId,那么我们怎么进行区分呢?

首选看源码:String pathVar() default ValueConstants.DEFAULT_NONE;

这里发现我们有一个属性可以进行定义,通过pathVar来操作:

 // /boss/1;age=18/2;age=20
 @GetMapping("/boss/{bossId}/{empId}")
 public Map get(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
                @MatrixVariable(value = "age",pathVar = "empAge")Integer empAge){
     Map<String, Object> map = new HashMap<>();
     map.put("bossAge",bossAge);
     map.put("empAge",empAge);
     return map;
 }
  1. 返回值处理

    1. 返回值处理器的判断是否支持这种类型返回值 supportsReturnType

    2. 返回值处理器调用 HandlerReturnValue 进行处理

    3. RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody注解的

      • 利用内容协商(浏览器默认会议请求头的方式告诉服务器它能接收什么样的返回值类型)

      • 服务器最终根据自己自身的能力,决定服务器能生产什么样内容类型的处理

      • springmvc会挨个遍历所有容器底层的 HttpMessageConverter,看谁能处理

        • 得到 MappingJackson2HttpMessageConverter 可以把数据写出去

        • 利用MappingJackson2HttpMessageConverter 将对象转为json再写出去!

6.9、内容协商

内容协商

根据客户端接收能力不同,返回不同媒体类型的数据。

1、引入依赖

  <dependency>
     <groupId>com.fasterxml.jackson.dataformat</groupId>
     <artifactId>jackson-dataformat-xml</artifactId>
 </dependency>

2、postman分别测试返回json和xml

只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。

  • xml类型:application/xml

  • json类型:application/json

3、开启浏览器参数方式内容协商功能

为了方便内容协商,开启基于请求参数的内容协商功能。

 spring:
   mvc:
     contentnegotiation:
       favor-parameter: true #开启请求参数内容协商模式 

发请求:

确定客户端接收什么样的内容类型;

1、Parameter策略优先确定是要返回json数据(获取请求头中的format的值)

2、最终进行内容协商返回给客户端json即可。

4、内容协商原理

  1. 判断当前响应头中是否已经有确定的媒体类型。MediaType

  2. 获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)【application/xml】

  • contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略

  • HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型

  1. 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)

  1. 找到支持操作Person的converter,把converter支持的媒体类型统计出来。

  2. 客户端需要【application/xml】。服务端能力【10种、json、xml】

  3. 进行内容协商的最佳匹配媒体类型

  4. 用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。 导入了jackson处理xml的包,xml的converter就会自动进来

5、自定义 MessageConverter :实现多协议数据兼容。json、xml、…

  1. @ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理

  2. Processor 处理方法返回值。通过 MessageConverter 处理

  3. 所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)

  4. 内容协商找到最终的 messageConverter;

SpringMVC的什么功能。一个入口给容器中添加一个 WebMvcConfigurer

  @Bean
     public WebMvcConfigurer webMvcConfigurer(){
         return new WebMvcConfigurer() {
             @Override
             public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
             }
         }
     }

如:先创建自己需要返回的类型,converter

 package com.zm.boot.converter;
 ​
 import com.zm.boot.bean.Person;
 import org.springframework.http.HttpInputMessage;
 import org.springframework.http.HttpOutputMessage;
 import org.springframework.http.MediaType;
 import org.springframework.http.converter.HttpMessageConverter;
 import org.springframework.http.converter.HttpMessageNotReadableException;
 import org.springframework.http.converter.HttpMessageNotWritableException;
 ​
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
 ​
 public class MessageConverter implements HttpMessageConverter<Person> {
     /**
      * 把aClass类型的数据读成mediaType格式类型
      */
     @Override
     public boolean canRead(Class<?> aClass, MediaType mediaType) {
         return false;
     }
 ​
     /**
      * 写,把符合aClass类型的数据写成mediaType格式n
      */
     @Override
     public boolean canWrite(Class<?> aClass, MediaType mediaType) {
         return aClass.isAssignableFrom(Person.class);//如果是person类型,就写成mediaType的格式
     }
 ​
     /**
      * 服务器要统计所有MessageConverter都能写出那些数据类型
      *
      * 如:application/x-guigu\
      */
     @Override
     public List<MediaType> getSupportedMediaTypes() {
         return MediaType.parseMediaTypes("application/x-guigu");
     }
 ​
     @Override
     public Person read(Class<? extends Person> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
         return null;
     }
 ​
     @Override
     public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
         //自定义协议数据的写出
         String data=person.getUserName()+";"+person.getAge()+";"+person.getBirth();
 ​
         //写出去
         OutputStream body=httpOutputMessage.getBody();
         body.write(data.getBytes());
     }
 }
 ​

将创建好的converter加入到系统中,在webconfig中进行添加

 @Configuration(proxyBeanMethods = false)
 public class WebConfig {
 ​
     @Bean
     public WebMvcConfigurer webMvcConfigurer(){
         return new WebMvcConfigurer() {
 ​
             //自定义内容协商策略;将自定义的converter设置访问方式
             @Override
             public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                 //Map<String,MediaType> mediaTypes
                 Map<String, MediaType> mediaTypeMap=new HashMap<>();
                 mediaTypeMap.put("json",MediaType.APPLICATION_JSON);
                 mediaTypeMap.put("xml",MediaType.APPLICATION_XML);
                 mediaTypeMap.put("gg",MediaType.parseMediaType("application/x-Jack"));//自定义访问方式
                 //指定支持解析哪些参数对应的那些媒体类型
                 ParameterContentNegotiationStrategy parameterStrategy=new ParameterContentNegotiationStrategy(mediaTypeMap);//请求参数的方式
                 //parameterStrategy.setParameterName("");//这是设置请求头的参数,如若没有设置,就是默认的format;如若设置了,就是设置的值,如设置成ff,则访问时就是ff=xml这样的方式
                 HeaderContentNegotiationStrategy headStrategy=new HeaderContentNegotiationStrategy();//请求头的方式
                 configurer.strategies(Arrays.asList(parameterStrategy,headStrategy));
             }
 ​
             //将自定义的converter添加进去
             @Override
             public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
                 converters.add(new MessageConverter());
             }
         };
     }
 }

6.10、异常处理

1、默认规则

  • 默认情况下,Spring Boot提供/error处理所有错误的映射

  • 对于机器客户端,它将生成ISON响应, 其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“whitelabel"错误视图,以HTML格式呈现相同的数据

  • 要对其进行自定义,添加View解析为error

  • 要完全替换默认行为,可以实现ErrorController 井注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。

2、定制错误处理逻辑

  • 自定义错误页

    • error/404.html | error/ 5xx.html 【在这个路径下的error会被自动解析!】有精确的错误状态码页面就精确匹配,没有就找4xx.html,如果都没有就返回白页!

  • @ControllerAdvice+ @ExceptionHandler处理全局异常 | 底层是:ExceptionHandlerExceptionResolver支持!

     package com.Jack.controller;
     ​
     import lombok.extern.slf4j.Slf4j;
     import org.springframework.web.bind.annotation.ControllerAdvice;
     import org.springframework.web.bind.annotation.ExceptionHandler;
     ​
     /**
      * 处理整个web  controller的异常
      */
     @Slf4j
     @ControllerAdvice
     public class GlobalExceptionHandler {
     ​
         @ExceptionHandler({NullPointerException.class,ArithmeticException.class})
         public String HandlerException(Exception e){
     ​
             log.error("异常是{}",e);
             return "login";//视图地址
         }
     }
  • @ResponseStatus + 自定义异常;底层是:ResponseStatus ExceptionResolver,把responsestatus注解的信息组装成ModelAndView返回;底层调用response.sendError(statusCode,resolvedReason);这是tomcat处理的/error

  • Spring底层的异常,如参数类型转换的异常:DefaultHandlerExceptionResolver 处理框架底层的异常!

    • response.sendError(HttpServletResponse.SC_BAD_REQUEST,ex.getmessage());

  • 自定义实现 HandlerExceptionHandler 处理异常 这里发现根本轮不到我们的异常处理,所以使用注解:@Order(优先级,数字越小优先级越高),可以作为全局的异常处理规则!

  • ErrorViewResolver 实现自定义处理异常

    • response.sendError. error请求就会转给controller

    • 你的异常没有任何人可以进行处理。tomcat底层response.sendError 。error请求就会转给controller

    • basicErrorController要去的地面地址是 ErrorViewReslover

3、异常处理自动配置原理

  • ErrorAutoConfiguration 自动配置了异常处理的规则!

    • 容器中的组件:类型:DefaultErrorAttributes - ->Id: errorAttributes

      • public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {}

      • DefaultErrorAttributes:定义错误页面中可以包含哪些属性!

    • 容器中的组件:类型:BasicErrorController- - > Id:basicErrorController(白页+JSON适配响应!)

      • 处理默认 /error 路径的请求 | 页面响应: new ModelAndView("error", model);

      • 容器中有组件View- - >Id:error(响应默认错误页)

      • 容器中放组件 BeanNameViewResolver(视图解析器)按照返回的视图名作为组件的Id去容器中找View对象。

    • 容器中的组件:类型:DefaultErrorViewResolverver- - >Id:conventionErrorViewResolver

      • 如果发生错误,会以HTPP的状态码作为视图页地址(viewName),找到真正的地址!

      • error/4xx、/ 5xx.html

如果想要返回页面,就会找error视图【StaticView】。默认是个白页!

这种返回的是JSON!

 @RequestMapping
 public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {}

这个返回的是页面:

 @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
 public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    HttpStatus status = getStatus(request);
    Map<String, Object> model = Collections
          .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
 }

4、异常处理步骤执行方法

  1. 执行目标方法,目标方法运行期间有任何异常都会被catch ,并且用dispatchException

  2. 进入视图解析流程(页面渲染?)

processDispatchResult(processdRequest,response,mappedHandler,mv,idspatchException);

  1. mv = processHandlerException; 处理handler发生的异常,处理完成返回ModelAndView

    1. 遍历所有的habdlerExceptionReslovers,看谁能处理当前的异常【HabdlerExceptionReslover处理异常解析器】

    2. 系统默认的异常解析器

  2. DefaultErrorAttributes 先来处理异常,把异常信息保存到request域,并且返回null

  3. 默认没有任何人能处理异常,所以异常会被抛出!

    1. 如果没有任何人能处理,最终底层会发送/error,会被底层的BasicErrorController处理

    2. 解析错误视图:遍历所有的ErrorViewReslover看谁能解析这个异常

    3. 默认的 DefaultErrorViewReslover,作用是把响应的状态码作为错误页的地址,error/5xx.html

    4. 模板引擎最终响应这个页面!

web原生组件注入(Filter、Servlet、Listener)

1、使用Servlet API (/*是原生的写法,/**是spring的写法)

  • @WebServlet

  • @WebFilter

  • @Listener

  • @ServletComponentScan

@WebServlet(urlPatterns ="/my") + @ServletComponentScan(basePackages = "com.xxx")放在主启动类上配合使用

@WebFilter(urlPatterns ={"/css/,"/images/"}) + @ServletComponentScan(basePackages = "com.xxx")

@Listener + + @ServletComponentScan(basePackages = "com.xxx")

在这里我们发现一个问题,我们的页面是直接响应,没有经过spring的拦截器?

 

扩展DispatcherServlet如何注册进来

  • 容器中自动配置了 DispatcherServlet 属性绑定到 WebMVCProperties:spring.mvc

  • 通过servletRegistrationBean<DispatcherServlet> 把DispatcherServlet 配置进来!

  • 默认的映射是 / 路径

Tomcat- - Servlet:

多个Servlet都能处理到同一层路径,精确优先原则

A:/web

B:/web/b

2、使用RegistrationBean

  • @ServletRegistrationBean

  • @FilterRegistrationBean

  • @ServletListenerRegistrationBean

嵌入式Servlet容器

1、切换嵌入式Servlet容器

  • 默认支持的webServlet

    • Tomcat,Jetty, or Undertwo

    • ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器

  • 切换服务器

    • TomcatServletWebServerFactory,JettyServletWebServerFactory, orUndertowServletWebServerFactory

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
     <exclusions>
         <exclusion>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-tomcat</artifactId>
         </exclusion>
     </exclusions>
 </dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-undertow</artifactId><!--jetty-->
 </dependency>
  • 原理

    • springboot启动发现当前是web应用,web场景包导入 - tomcat

    • web应用会创建一个web的IOC容器!ServletWebServerApplicationContext

    • ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(servlet的web服务器工厂- - >Servlet的web服务器)

    • springboot底层默认有很多的webServer工厂:

      TomcatServletWebServerFactory,JettyServletWebServerFactory, orUndertowServletWebServerFactory

    • 底层会有一个自动配置类:ServletWebServerFactoryAutoConfiguration

    • ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration

    • ServletWebServerFactoryConfiguration 配置类,根据动态判断系统中到底导入了哪个web服务器的包。(默认是web-starter导入tomcat包),容器中就有:TomcatServletWebServerFactory

    • TomcatServletWebServerFactory:创建出Tomcat并启动!TomcatWebServer的构造器拥有:initialize- - >this.tomcat.start();

    • 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在!)

 

2、定制Servlet容器

  • 实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>

    • 把配置文件的值与ServletWebServerFactory 进行绑定!

  • 修改配置文件server.xxx

  • 直接自定义 ConfigurableServletWebServerFactory

 

xxxxCustomizer:定制化器,可以改变xxxx的默认规则!

 import org.springframework.boot.web.server.WebServerFactoryCustomizer;
 import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
 import org.springframework.stereotype.Component;
 ​
 @Component
 public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
 ​
     @Override
     public void customize(ConfigurableServletWebServerFactory server) {
         server.setPort(9000);
     }
 ​
 }

定制化原理

1、定制化的常见方式

  • 修改配置文件

  • xxxxCustomizer:定制化器

  • 编写自定义的配置类 xxxxConfiguration + @Bean替换,增加容器中默认组件:视图解析器

  • Web应用编写一个配置类 实现 WebMvcConfiguration ,即可定制化web功能 + @Bean给容器中扩展一些组件!

     @Configuration
     public class webConfig implements WebMvcConfiguration {}
  • @EnableWebMvc + WebMvcConfigurer - - @Bean 可以全面接管SpringMvc,所有规则全部自己进行重新配置;实现定制和拓展!

    • @EnableWebMvc 这里一旦全面接管spring mvc所有的静态资源,视图解析器,欢迎页….所有的全部失效!

    • 原理

      1. WebMvcAutoConfiguration ,默认的springmvc的自动配置类:静态资源,欢迎页…

      2. 一旦使用@EnableWebMvc,会@Import(DelegatingWebMvcConfiguration.class)

      3. DelegatingWebMvcConfiguration的作用:只保证springmvc最基本的使用!

        • 把系统中的WebMvcConfiguration拿过来。所有功能的定制都是这些WebMvcConfiguration 合起来一起生效!

        • 自动配置了一些非常底层的组件。RequestMappingHandlerMapping这些组件依赖的组件都是从容器中获取

        • public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {}

      4. WebMvcAutoConfiguration :里面的配置类要能全部生效必须@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

      5. @EnableWebMvc 导致了WebMvcAutoConfiguration 没有生效!

  • ……

 

2、原理分析套路

场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties - 绑定配置文件项!

 

Junit5 注解

官网:https://docs.spring.io/spring-framework/docs/5.3.19/reference/html/testing.html#spring-testing-annotation-commit

所有支持的注解都在包 org.junit.jupiter.api 下;

使用@Test、@TestTemplate、@RepeatedTest、@BeforeAll、@AfterAll、@BeforeEach或@AfterEach注释的方法不能返回值。

@Test 表示方法是测试方法。与JUnit 4的@Test注释不同,这个注释不声明任何属性,因为JUnit Jupiter中的测试扩展基于它们自己的专用注释进行操作。 这些方法是继承的,除非它们被重写。

@ParameterizedTest 表示方法是参数化测试。这些方法是继承的,除非它们被重写。

@RepeatedTest 表示方法是重复测试的测试模板。这些方法是继承的,除非它们被重写。

@TestFactory 表示方法是动态测试的测试工厂。这些方法是继承的,除非它们被重写。

@TestInstance 用于为带注释的测试类配置测试实例生命周期。这些方法是继承的,除非它们被重写。

@TestTemplate 表示方法是为测试用例设计的模板,根据注册提供程序返回的调用上下文的数量进行多次调用。这些方法是继承的,除非它们被重写。

@DisplayName 声明测试类或测试方法的自定义显示名称。这样的注解不是继承来的。

@BeforeEach 表示在当前类中每个@Test、@RepeatedTest、@ParameterizedTest或@TestFactory方法之前执行注释的方法;类似于JUnit 4的@Before。

这些方法是继承的,除非它们被重写。

@AfterEach 表示在当前类中的每个@Test、@RepeatedTest、@ParameterizedTest或@TestFactory方法之后,都应该执行带注释的方法;类似于JUnit 4的@After。

这些方法是继承的,除非它们被重写。

@BeforeAll 表示应在当前类中的所有@Test、@RepeatedTest、@ParameterizedTest和@TestFactory方法之前执行带注释的方法;类似于JUnit 4的@BeforeClass。

这些方法是继承的(除非它们被隐藏或覆盖),并且必须是静态的(除非使用“每个类”测试实例生命周期)。

@AfterAll 表示在当前类中,所有@Test、@RepeatedTest、@ParameterizedTest和@TestFactory方法都应该执行注释的方法;类似于JUnit 4的@AfterClass。

这些方法是继承的(除非它们被隐藏或覆盖),并且必须是静态的(除非使用“每个类”测试实例生命周期)。

@Nested 表示带注释的类是一个嵌套的、非静态的测试类。@BeforeAll和@AfterAll方法不能直接在 @Nested 测试类中使用,除非使用“每个类”测试实例生命周期。

这样的注解不是继承来的。

@Tag 用于在类或方法级别声明过滤测试的标记;类似于TestNG中的测试组或JUnit 4中的类别。

此类注释在类级别继承,但在方法级别继承。

@Disabled 用于禁用测试类或测试方法;类似于JUnit 4的@Ignore。

这样的注解不是继承来的。

@ExtendWith 用于注册自定义扩展。这些注释是继承。

元注释,注释 JUnit Jupiter注解可以用作元注解。这意味着您可以定义自己的组合注释,它将自动继承其元注释的语义。

例如,您可以创建一个名为 @Fast 的自定义组合注释, 如下所示,而不是在整个代码库中复制和粘贴@Tag(“fast”)(请参阅标记和过滤)。然后可以使用@Fast作为@Tag(“fast”)的drop-in替代。

 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 ​
 import org.junit.jupiter.api.Tag;
 ​
 @Target({ ElementType.TYPE, ElementType.METHOD })
 @Retention(RetentionPolicy.RUNTIME)
 @Tag("fast")
 public @interface Fast {}

 

断言(assertions)机制

  • 断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:

  • 检查业务逻辑返回的数据是否合理。

  • 所有的测试运行结束以后,会有一个详细的测试报告;

    1、简单断言

    用来对单个值进行简单的验证。如:

 /**
      *断言:前面断言失败,后面的代码都不会执行
      */
     @DisplayName("测试简单断言")
     @Test
     void testSimpleAssertions(){
         int cal = cal(2, 3);
         assertEquals(5,cal ,"业务逻辑失败");
 ​
         Object o = new Object();
         Object o1 = new Object();
         assertSame(o,o1,"两个对象不一样");
     }

2、数组断言

通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等

    @Test
     @DisplayName("array assertion")
     public void array() {
         assertArrayEquals(new int[]{2, 2}, new int[] {1, 2},"数组内容不相等");
     }

3、组合断言

assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言

 @Test
 @DisplayName("组合断言")
 void all() {
     //所有断言全部需要成功
     assertAll("test",
             () -> assertTrue(true && true),
             () -> assertEquals(1, 2));
 }

4、异常断言

在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。

 @Test
 @DisplayName("异常断言")
 //断言业务逻辑一定出现异常
 public void exceptionTest() {
     assertThrows(ArithmeticException.class,() ->{int i =1/0;},"业务逻辑居然正常运行?");
 }

5、超时断言

Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间

 @Test
 @DisplayName("超时断言")
 public void timeoutTest() {
     //如果测试方法时间超过1s将会异常
     Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
 }

6、快速失败

通过 fail 方法直接使得测试失败

 @Test
 @DisplayName("快速断言")
 public void shouldFail() {
     fail("测试失败");
 }

Assertions和Assumptions简介

Assumptions和Assertions容易混淆,因此这里通过对比它们来学习:

Assertions即断言类,里面提供了很多静态方法,例如assertTrue,如果assertTrue的入参为false,就会抛出AssertionFailedError异常,Junit对抛出此异常的方法判定为失败;

Assumptions即假设类,里面提供了很多静态方法,例如assumeTrue,如果assumeTrue的入参为false,就会抛出TestAbortedException异常,Junit对抛出此异常的方法判定为跳过;

简单的说,Assertions的方法抛出异常意味着测试不通过,Assumptions的方法抛出异常意味着测试被跳过(为什么称为"跳过"?因为mvn test的执行结果被标记为Skipped);

嵌套测试

嵌套测试可以帮助我们对测试结构进行分层。借助于Java嵌套类的语法,JUnit5可以通过@Nested注解,实现嵌套测试,示例:

 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
  
 import java.util.EmptyStackException;
 import java.util.Stack;
  
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
  
 @DisplayName("A stack")
 class TestingAStackDemo {
  
     Stack<Object> stack;
  
     @Test
     @DisplayName("is instantiated with new Stack()")
     void isInstantiatedWithNew() {
         new Stack<>();
     }
  
     @Nested
     @DisplayName("when new")
     class WhenNew {
  
         @BeforeEach
         void createNewStack() {
             stack = new Stack<>();
         }
  
         @Test
         @DisplayName("is empty")
         void isEmpty() {
             assertTrue(stack.isEmpty());
         }
  
         @Test
         @DisplayName("throws EmptyStackException when popped")
         void throwsExceptionWhenPopped() {
             assertThrows(EmptyStackException.class, stack::pop);
         }
  
         @Test
         @DisplayName("throws EmptyStackException when peeked")
         void throwsExceptionWhenPeeked() {
             assertThrows(EmptyStackException.class, stack::peek);
         }
  
         @Nested
         @DisplayName("after pushing an element")
         class AfterPushing {
  
             String anElement = "an element";
  
             @BeforeEach
             void pushAnElement() {
                 stack.push(anElement);
             }
  
             @Test
             @DisplayName("it is no longer empty")
             void isNotEmpty() {
                 assertFalse(stack.isEmpty());
             }
  
             @Test
             @DisplayName("returns the element when popped and is empty")
             void returnElementWhenPopped() {
                 assertEquals(anElement, stack.pop());
                 assertTrue(stack.isEmpty());
             }
  
             @Test
             @DisplayName("returns the element when peeked but remains not empty")
             void returnElementWhenPeeked() {
                 assertEquals(anElement, stack.peek());
                 assertFalse(stack.isEmpty());
             }
         }
     }
 }

 

6、参数化测试

参数化测试是JUnit5很重要的一个 新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。

利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参 数就新增个单元测试,省去了很多冗余代码。

@ValueSource为参数化测试指定入参来源,支持八大基础类以及String类型Cass类型@NullSource表示为参数化测试提供一个nul的入参

@EnumSource表示为参数化测试提供一个枚举入参

@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参

@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YMLJSON文件甚至方法的返回值也可以作为入参。只需要去实现

ArgumentsProvider接口,任何外部文件都可以作为它的入参。

 

SpringBoot指标监控

  1. 简介


未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。

  1. 使用


2.1 引入场景依赖

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

2.2 访问网址

http://localhost:8080/actuator/**

2.3 暴露所有监控信息为HTTP

 # management 是所有actuator的配置
 # management.endpoint.端点名.xxxx  对某个端点的具体配置
 management:
   endpoints:
     enabled-by-default: true  #暴露所有断电信息
     web:
       exposure:
         include: '*'  #以web方式暴露
    endpoint:
     health:
       show-details: always  #显示某一个端点的详细信息

测试:

http://localhost:8080/actuator/beans

http://localhost:8080/actuator/configprops

http://localhost:8080/actuator/metrics/application.ready.time

http://localhost:8080/actuator/endpointName/detailPath

3.Actuator EndPoint

3.1最常使用的端点:

3.1最常使用的端点:

ID 描述
auditevents 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件。
beans 显示应用程序中所有Spring Bean的完整列表。
caches 暴露可用的缓存。
conditions 显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops 显示所有@ConfigurationProperties。
env 暴露Spring的属性ConfigurableEnvironment
flyway 显示已应用的所有Flyway数据库迁移。需要一个或多个Flyway组件。
health 显示应用程序运行状况信息。
httptrace 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。
info 显示应用程序信息。
integrationgraph 显示Spring integrationgraph 。需要依赖spring-integration-core。
loggers 显示和修改应用程序中日志的配置。
liquibase 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
metrics 显示当前应用程序的“指标”信息。
mappings 显示所有@RequestMapping路径列表。
scheduledtasks 显示应用程序中的计划任务。
sessions 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。
shutdown 使应用程序正常关闭。默认禁用。
startup 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置
threaddump 执行线程转储。

如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:

ID 描述
heapdump 返回hprof堆转储文件。
jolokia 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core。
logfile 返回日志文件的内容(如果已设置logging.file.name或logging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus。

最常用的Endpoint

  • Health:监控状况

  • Metrics:运行时指标

  • Loggers:日志记录

3.2 Health Endpoint

健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。

重要的几点:

  • health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告

  • 很多的健康检查默认已经自动配置好了,比如:数据库、redis等

  • 可以很容易的添加自定义的健康检查机制

3.3 Metrics Endpoint

提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到

  • 通过Metrics对接多种监控系统

  • 简化核心Metrics开发

  • 添加自定义Metrics或者扩展已有Metrics

3.4管理Endpoints

3.4.1开启与禁用Endpoints

  • 默认所有的Endpoint除过shutdown都是开启的。

  • 需要开启或者禁用某个Endpoint。配置模式为 management.endpoint..enabled = true

 management:
   endpoint:
     beans:
       enabled: true

或者禁用所有的Endpoint然后手动开启指定的Endpoint

 management:
   endpoints:
     enabled-by-default: false
   endpoint:
     beans:
       enabled: true
     health:
       enabled: true

4.定制 Endpoint

4.1定制Health信息

 @Component
 //点点名为截取前半部分 MyCom
 public class MyComHealthIndicator extends AbstractHealthIndicator {
 ​
     /**
      * 真实的检查方法
      * @param builder
      * @throws Exception
      */
     @Override
     protected void doHealthCheck(Health.Builder builder) throws Exception {
         //mongodb。  获取连接进行测试
         Map<String,Object> map = new HashMap<>();
         // 检查完成
         if(1 == 2){
 //            builder.up(); //健康
             builder.status(Status.UP);
             map.put("count",1);
             map.put("ms",100);
         }else {
 //            builder.down();
             builder.status(Status.OUT_OF_SERVICE);
             map.put("err","连接超时");
             map.put("ms",3000);
         }
 ​
 ​
         builder.withDetail("code",100)
                 .withDetails(map);
 ​
     }
 }

4.3 定制Metrics信息

4.3.1 SpringBoot支持自动适配的Metrics

  • JVM metrics, report utilization of:

    • Various memory and buffer pools

    • Statistics related to garbage collection

    • Threads utilization

    • Number of classes loaded/unloaded

  • CPU metrics

  • File descriptor metrics

  • Kafka consumer and producer metrics

  • Log4j2 metrics: record the number of events logged to Log4j2 at each level

  • Logback metrics: record the number of events logged to Logback at each level

  • Uptime metrics: report a gauge for uptime and a fixed gauge representing the application’s absolute start time

  • Tomcat metrics (server.tomcat.mbeanregistry.enabled must be set to true for all Tomcat metrics to be registered)

  • Spring Integration metrics

4.3.2 增加定制Metrics

 @Service
 public class CityServiceImpl implements CityService {
 ​
     @Autowired
     CityMapper cityMapper;
 ​
     Counter counter;
 ​
     public CityServiceImpl(MeterRegistry meterRegistry){
         counter = meterRegistry.counter("cityService.saveCity.count");
     }
 ​
     public void saveCity(City city) {
         counter.increment();
         cityMapper.insert(city);
 ​
     }
 }

4.4 定制Endpoint

 @Component
 @Endpoint(id = "container")
 public class DockerEndpoint {
 ​
 ​
     @ReadOperation
     public Map getDockerInfo(){
         return Collections.singletonMap("info","docker started...");
     }
 ​
     @WriteOperation
     private void restartDocker(){
         System.out.println("docker restarted....");
     }
 ​
 }

5. 指标监控可视化

GitHub:https://github.com/codecentric/spring-boot-admin

 

外部化配合

1、外化配置

  1. Default properties (specified by setting SpringApplication.setDefaultProperties).

  2. @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.

  3. Config data (such as application.properties files).

  4. A RandomValuePropertySource that has properties only in random.*.

  5. OS environment variables.

  6. Java System properties (System.getProperties()).

  7. JNDI attributes from java:comp/env.

  8. ServletContext init parameters.

  9. ServletConfig init parameters.

  10. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).

  11. Command line arguments.

  12. properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.

  13. @TestPropertySource annotations on your tests.

  14. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.

2、访问命令行配置

默认情况下,SpringApplication将任何命令行选项参数(即以 开头的参数--,例如--server.port=9000)转换为 aproperty并将它们添加到 Spring Environment。如前所述,命令行属性始终优先于基于文件的属性源。

如果您不想将命令行属性添加到 中Environment,可以使用 禁用它们SpringApplication.setAddCommandLineProperties(false)

3、外部应用属性

Spring Boot will automatically find and load application.properties and application.yaml files from the following locations when your application starts:

  1. From the classpath

    1. The classpath root

    2. The classpath /config package

  2. From the current directory

    1. The current directory

    2. The /config subdirectory in the current directory

    3. Immediate child directories of the /config subdirectory

  java -jar myproject.jar --spring.config.name=myproject

7、模板引擎

7.1、Thymeleaf

模板引擎

前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。

jsp支持非常强大的功能,包括能写Java代码,但是呢,我们现在的这种情况,SpringBoot这个项目首先是以jar的方式,不是war,像第二,我们用的还是嵌入式的Tomcat,所以呢,他现在默认是不支持jsp的

那不支持jsp,如果我们直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦,那怎么办呢?

SpringBoot推荐你可以来使用模板引擎:

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

 

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

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

引入Thymeleaf

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

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

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

Spring官方文档:找到我们对应的版本

https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter

找到对应的pom依赖:可以适当点进源码看下本来的包!

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

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

 

 

Thymeleaf 分析

前面呢,我们已经引入了Thymeleaf,那这个要怎么使用呢?

我们首先得按照SpringBoot的自动配置原理看一下我们这个Thymeleaf的自动配置规则,在按照那个规则,我们进行使用。

我们去找一下Thymeleaf的自动配置类:ThymeleafProperties

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

我们可以在其中看到默认的前缀和后缀!

我们只需要把我们的html页面放在类路径下的templates下,thymeleaf就可以帮我们自动渲染了。

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

测试:

1、编写一个TestController

@Controller
public class TestController {
    
    @RequestMapping("/t1")
    public String test1(){
        //classpath:/templates/test.html
        return "test";
    }
    
}

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


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

</body>
</html>

3、启动项目请求测试

 

Thymeleaf 语法学习

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

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

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

1、修改测试请求,增加数据传输;

@RequestMapping("/t1")
public String test1(Model model){
    //存入数据
    model.addAttribute("msg","<h1>Hello,Thymeleaf!</h1>");
    //classpath:/templates/test.html
    return "test";
}

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

我们可以去官方文档的中看一下命名空间拿来过来:

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

3、我们去编写下前端页面

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

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

4、启动测试!

 

 

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

1、我们可以使用任意的 th:attr 来替换Html中原生属性的值!

 

2、我们能写哪些表达式呢?

Simple expressions:(表达式语法)
Variable Expressions: ${...}:获取变量值;OGNL;
    1)、获取对象的属性、调用方法
    2)、使用内置的基本对象:#18
         #ctx : the context object.
         #vars: the context variables.
         #locale : the context locale.
         #request : (only in Web Contexts) the HttpServletRequest object.
         #response : (only in Web Contexts) the HttpServletResponse object.
         #session : (only in Web Contexts) the HttpSession object.
         #servletContext : (only in Web Contexts) the ServletContext object.

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

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

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

练习测试:

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

    @RequestMapping("/test")
    public String test(Model model){
        model.addAttribute("msg","<h1>hello,Thymeleaf!</h1>");

        model.addAttribute("users", Arrays.asList("qinjiang","kuangshen"));
        return "test";
    }
}

2、测试页面取出数据

<!DOCTYPE html>
<html lang="en">
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <h1>test页面</h1>
    <p th:text="${msg}" ></p>
    <!-- 不转义 -->
    <p th:utext="${msg}" ></p>
    <!--遍历数据-->
    <h3 th:each="user:${users}" th:text="${user}"></h3>
<!--    <h3 th:each="user:${users}">[[ ${user} ]]</h3>-->
</div>
</body>
</html>

3、启动项目测试!

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

7.2、视图解析原理流程

  1. 目标方法处理的过程中,所有数据的都会放在 ModelAndViewContainer 里面,包括数据和视图地址。

  2. 方法的参数就是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer

  3. 任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)

  4. processDispatchResult 处理派生结果(页面如何访问)

    1. render(mv,request,response); 进行页面的渲染逻辑

      • 1、根据方法的String返回值得到View对象 【定义了页面的渲染逻辑】

        • 1、所有的视图解析器尝试是否能根据当前返回值得到View对象

        • 2、根据返回结果得到 redirect:/main.html - -> Thymeleaf new RedirectView()

        • 3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面视图解析器,得到视图对象

        • 4、view.render(mv,getModelInternal(),request,response); 视图对象调用render进行页面渲染工作

          • RedirectView 如何进行页面渲染 【重定向到一个页面】

          • 1、获得目标url地址

          • 2、response.sendRedirect(encodedURL) ;

视图解析:

  • 返回值以 forword 开始:new InternalResourceView(forwordUrl); - ->转发 request.getRequestDispatcher(path).forword(request,response);

  • 返回值以 redirect 开始:new RedirectView(); - ->render就是重定向

  • 返回值是普通字符串: new ThymeleafView(); - ->

8、MVC自动配置原理

8.1、官网阅读

在进行项目编写前,我们还需要知道一个东西,就是SpringBoot对我们的SpringMVC还做了哪些配置,包括如何扩展,如何定制。

只有把这些都搞清楚了,我们在之后使用才会更加得心应手。途径一:源码分析,途径二:官方文档!

地址 :https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration

Spring MVC Auto-configuration
// Spring Boot为Spring MVC提供了自动配置,它可以很好地与大多数应用程序一起工作。
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
// 自动配置在Spring默认设置的基础上添加了以下功能:
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 
// 自动注册了Converter:
// 转换器,这就是我们网页提交数据到后台自动封装成为对象的东西,比如把"1"字符串自动转换为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).

/*
如果您希望保留Spring Boot MVC功能,并且希望添加其他MVC配置(拦截器、格式化程序、视图控制器和其他功能),则可以添加自己
的@configuration类,类型为webmvcconfiguer,但不添加@EnableWebMvc。如果希望提供
RequestMappingHandlerMapping、RequestMappingHandlerAdapter或ExceptionHandlerExceptionResolver的自定义
实例,则可以声明WebMVCregistrationAdapter实例来提供此类组件。
*/
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration 
(interceptors, formatters, view controllers, and other features), you can add your own 
@Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide 
custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or 
ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

// 如果您想完全控制Spring MVC,可以添加自己的@Configuration,并用@EnableWebMvc进行注释。
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

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

8.2、ContentNegotiatingViewResolver 内容协商视图解析器

自动配置了ViewResolver,就是我们之前学习的SpringMVC的视图解析器;

即根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。

我们去看看这里的源码:我们找到 WebMvcAutoConfiguration , 然后搜索ContentNegotiatingViewResolver。找到如下方法!

@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
    ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
    resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
    // ContentNegotiatingViewResolver使用所有其他视图解析器来定位视图,因此它应该具有较高的优先级
    resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return resolver;
}

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

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

我们继续点进去看,他是怎么获得候选的视图的呢?

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

Iterator var5 = this.viewResolvers.iterator();

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

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

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

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

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

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

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

2、怎么看我们自己写的视图解析器有没有起作用呢?

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

 

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

找到this

 

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

 

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

8.3、转换器和格式化器

找到格式化转换器:

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

点击去:

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

/**
* Date format to use. For instance, `dd/MM/yyyy`. 默认的
 */
private String dateFormat;

可以看到在我们的Properties文件中,我们可以进行自动配置它!

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

 

8.4、修改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;

//如果我们要拓展springmvc ,官方建议我们这样做!
//应为类型要求为WebMvcConfigurer,所以我们实现其接口
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

  	//视图跳转
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/kuang").setViewName("test");
    }
}

我们去浏览器访问一下:

 

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

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

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

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

3、我们点进EnableWebMvcConfiguration这个类看一下,它继承了一个父类:DelegatingWebMvcConfiguration

这个父类中有这样一段代码:

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

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

protected void addViewControllers(ViewControllerRegistry registry) {
    this.configurers.addViewControllers(registry);
}

5、我们点进去看一下

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

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

}

所以得出结论:所有的WebMvcConfiguration都会被作用,不止Spring自己的配置类,我们自己的配置类当然也会被调用;

8.5、全面接管SpringMVC

官方文档

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

全面接管即:SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己去配置!

只需在我们的配置类中要加一个@EnableWebMvc。

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

不加注解之前,访问首页:

 

给配置类加上注解:@EnableWebMvc

 

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

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

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

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

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

2、它继承了一个父类 WebMvcConfigurationSupport

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

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

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

总结一句话:@EnableWebMvc将WebMvcConfigurationSupport组件导入进来了;

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

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

 

9、页面国际化

有的时候,我们的网站会去涉及中英文甚至多语言的切换,这时候我们就需要学习国际化了!

准备工作

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

 

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

配置文件编写

1、我们在resources资源文件下新建一个i18n目录,存放国际化配置文件

2、建立一个login.properties文件,还有一个login_en_US.properties;发现IDEA自动识别了我们要做国际化操作;文件夹变了!

 

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

 

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

 

这样就快捷多了!

 

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

 

如果没有这个,就去plugins下载一个插件:Resource Bundle Editor

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

 

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

login.properties :默认

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

英文:

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

中文:

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

OK,配置文件步骤搞定!

配置文件原理探究

我们去看一下SpringBoot对国际化的自动配置!这里又涉及到一个类:MessageSourceAutoConfiguration

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

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

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

spring.messages.basename=i18n.login

配置页面国际化值

去页面获取国际化的值,查看Thymeleaf的文档,找到message取值操作为:#{...}。我们去页面测试下:

 

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

 

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

配置国际化解析

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

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

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

AcceptHeaderLocaleResolver 这个类中有一个方法

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

那假如我们现在想点击链接让我们的国际化资源生效,就需要让我们自己的Locale生效!

我们去自己写一个自己的LocaleResolver,可以在链接上携带区域信息!

修改一下前端页面的跳转连接:

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

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

package com.kuang.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) {

    }
}

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

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

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

 

10、整合JDBC

10.1、SpringData简介

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

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

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

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

https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter

10.2、整合JDBC

创建测试项目测试数据源

1、我去新建一个项目测试:springboot-data-jdbc ; 引入相应的模块!基础模块

 

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

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

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

3、编写yaml配置文件连接数据库:

spring:
  datasource:
    username: root
    password: "01032100"
    # 如果时区报错了,就增加一个时区的配置就OK 了 serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver

这里我们可能会遇到一个springboot的问题,那就是:Access denied for user 'root'@'localhost' (using password: YES),如果我们遇到这个问题,是跟我们的yaml文件有关系的,因为`yml是区分数据类型的给密码加引号,参考:https://blog.csdn.net/u012862619/article/details/80482485

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

@SpringBootTest
class Springboot05DataApplicationTests {

    //DI注入数据源
    @Autowired
    DataSource dataSource;
    @Test
    void contextLoads() throws SQLException {
    //    查看一下默认的数据源:class com.zaxxer.hikari.HikariDataSource
        System.out.println(dataSource.getClass());

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

        //xxx Template  :SpringBoot已经配置好的模板 bean,拿来即用 CRUD

        //关闭
        connection.close();
    }

}

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

我们来全局搜索一下,找到数据源的所有自动配置都在 :DataSourceAutoConfiguration文件:

@Import(
    {Hikari.class, Tomcat.class, Dbcp2.class, Generic.class, DataSourceJmxConfiguration.class}
)
protected static class PooledDataSourceConfiguration {
    protected PooledDataSourceConfiguration() {
    }
}

这里导入的类都在 DataSourceConfiguration 配置类下,可以看出 Spring Boot 2.2.5 默认使用HikariDataSource 数据源,而以前版本,如 Spring Boot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源;

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

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

关于数据源我们并不做介绍,有了数据库连接,显然就可以 CRUD 操作数据库了。但是我们需要先了解一个对象 JdbcTemplate

JDBCTemplate

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

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

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

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

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

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

  • execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;

  • update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;

  • query方法及queryForXXX方法:用于执行查询相关语句;

  • call方法:用于执行存储过程、函数相关语句。

 

测试

编写一个Controller,注入 jdbcTemplate,编写测试方法进行访问测试;

package com.kuang.controller;

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

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

@RestController
@RequestMapping("/jdbc")
public class JdbcController {

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

    //查询employee表中所有数据
    //List 中的1个 Map 对应数据库的 1行数据
    //Map 中的 key 对应数据库的字段名,value 对应数据库的字段值
    @GetMapping("/list")
    public List<Map<String, Object>> userList(){
        String sql = "select * from employee";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        return maps;
    }
    
    //新增一个用户
    @GetMapping("/add")
    public String addUser(){
        //插入语句,注意时间问题
        String sql = "insert into employee(last_name, email,gender,department,birth)" +
                " values ('狂神说','24736743@qq.com',1,101,'"+ new Date().toLocaleString() +"')";
        jdbcTemplate.update(sql);
        //查询
        return "addOk";
    }

    //修改用户信息
    @GetMapping("/update/{id}")
    public String updateUser(@PathVariable("id") int id){
        //插入语句
        String sql = "update employee set last_name=?,email=? where id="+id;
        //数据
        Object[] objects = new Object[2];
        objects[0] = "秦疆";
        objects[1] = "24736743@sina.com";
        jdbcTemplate.update(sql,objects);
        //查询
        return "updateOk";
    }

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

测试请求,结果正常;

到此,CURD的基本操作,使用 JDBC 就搞定了。

 

10.3、集成Druid

Druid简介

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

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

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

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

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

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

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

 

配置数据源

1、添加上Druid数据源依赖

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

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

spring:
  datasource:
    username: root
    password: "123456"
    # 如果时区报错了,就增加一个时区的配置就OK 了 serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource #自定义数据源

3、数据源切换之后,在测试类中注入 DataSource,然后获取到它,输出一看便知是否成功切换;

 

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

spring:
  datasource:
    username: root
    password: "123456"
    # 如果时区报错了,就增加一个时区的配置就OK 了 serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

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

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

5、导入Log4J 的依赖

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

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

package com.kuang.config;

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

import javax.sql.DataSource;

@Configuration
public class DruidConfig {

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

}

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

@SpringBootTest
class SpringbootDataJdbcApplicationTests {

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

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

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

        //关闭连接
        connection.close();
    }
}

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

 

配置Druid数据源监控

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

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

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

    // 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet 
    // 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
    Map<String, String> initParams = new HashMap<>();
    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;
}

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

 

进入之后

 

配置 Druid web 监控 filter 过滤器

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

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

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

平时在工作中,按需求进行配置即可,主要用作监控!

 

10.4、整合Mybatis

整合mybatis

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

Github:https://github.com/mybatis

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

 

整合测试

1、导入 MyBatis 所需要的依赖

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

2、配置数据库连接信息(不变)

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

#mybatis
mybatis.type-aliases-package=com.kuang.pojo
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
mybatis.configuration.map-underscore-to-camel-case=true #驼峰命名

3、测试数据库是否连接成功!

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

User.java

package com.kuang.pojo;

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

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

5、创建mapper目录以及对应的 Mapper 接口

UserMapper.interface

package com.kuang.mapper;

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

import java.util.List;

//这个注解表示了这是一个 mybatis 的mapper类;
@Mapper
@Repository
public interface UserMapper {

    List<User> queryUserList();

    User queryUserById(int id);

    int addUser(User user);

    int updateUser(User user);

    int deleteUser(int id);
}

6、对应的Mapper映射文件

UserMapper.xml

<!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="queryUserList" resultType="User">
        select * from mybatis.user
    </select>

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

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

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

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

</mapper>

7、maven配置资源过滤问题

    <!-- 如果不添加此节点mybatis的mapper.xml文件都会被漏掉。 -->
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.yml</include>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.yml</include>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
      <!-- 配置将哪些资源文件(静态文件/模板文件/mapper文件)加载到tomcat输出目录里-->
        <resource>
            <directory>src/main/java</directory><!--java文件的路径-->
            <includes>
                <include>**/*.*</include>
            </includes>
            <!-- <filtering>false</filtering>-->
        </resource>
        <resource>
            <directory>src/main/resources</directory><!--资源文件的路径-->
            <includes>
                <include>**/*.*</include>
            </includes>
            <!-- <filtering>false</filtering>-->
        </resource>
    </resources>

配置文件:

如果你要整合的需要用到Thymeleaf + mybatis ,那么请配置它!(你可以选择不配置,但是你会遇到下面的一个报错坑!)

#声明thymeleaf使用非严格的html。
spring.thymeleaf.content-type=text/html
spring.thymeleaf.mode=LEGACYHTML5
spring.thymeleaf.cache=false

8、编写UserController 进行测试!

package com.kuang.controller;

import com.kuang.mapper.UserMapper;
import com.kuang.pojo.User;
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("/queryUserList")
    public List<User> queryUserList(){
        List<User> userList = userMapper.queryUserList();
        for (User user : userList) {
            System.out.println(user);
        }
        return userList;
    }

    //提那家一个用户
    @GetMapping("/add")
    public String addUser(){
        userMapper.addUser(new User(5,"SAO","123456"));
        return "ok";
    }

    //修改一个用户
    @GetMapping("/update")
    public String updateUser(){
        userMapper.updateUser(new User(5,"TK","111111"));
        return "update";
    }

    //删除一个用户
    @GetMapping("del")
    public String deleteUser(){
        userMapper.deleteUser(5);
        return "del OK";
    }
}

启动项目访问进行测试!

总结:

  • 在这里如果使用注解进行开发,注意我们如果有复杂的语句的话是不够的,所以可以使用@Options进行扩展设置!

整合包

  • mybatis-spring-boot-start

步骤:

  1. 导入包

  2. 配置文件

  3. mybatis配置

  4. 编写sql

  5. service层调用dao层

  6. controller调用service

可能遇到的错误:

 

11、集成SpringSecurity

1、安全简介

在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。

市面上存在比较有名的:Shiro,Spring Security !

这里需要阐述一下的是,每一个框架的出现都是为了解决某一问题而产生了,那么Spring Security框架的出现是为了解决什么问题呢?

首先我们看下它的官网介绍:Spring Security官网地址:https://spring.io/projects/spring-security

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。

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

从官网的介绍中可以知道这是一个权限框架。想我们之前做项目是没有使用框架是怎么控制权限的?对于权限 一般会细分为功能权限,访问权限,和菜单权限。代码会写的非常的繁琐,冗余。

怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生而Spring Scecurity就是其中的一种。

Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

2、实战测试

实验环境的搭建

1、新建一个初始的springboot项目web模块,thymeleaf模块

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

2、导入静态资源

链接:https://gitee.com/ENNRIAAA/spring-security-material

3、controller跳转!

package com.kuang.controller;

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

@Controller
public class RouterController {

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

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

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

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

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

4、测试实验环境是否OK!

 

3、认识SpringSecurity

Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

  • WebSecurityConfigurerAdapter:自定义Security策略

  • AuthenticationManagerBuilder:自定义认证策略

  • @EnableWebSecurity:开启WebSecurity模式

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

“认证”(Authentication)

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

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

“授权” (Authorization)

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

这个概念是通用的,而不是只在Spring Security 中存在。

参考官网:https://spring.io/projects/spring-security,查看我们自己的项目中的版本,找到对应的帮助文档:https://docs.spring.io/spring-security/reference/index.html

认证和授权

目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能

1、引入 Spring Security 模块

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

2、编写 Spring Security 配置类

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

查看我们自己项目中的版本,找到对应的帮助文档:

https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5 #servlet-applications 8.16.4

 

3、编写基础配置类

package com.kuang.config;

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

@EnableWebSecurity // 开启WebSecurity模式
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       
  }
}

4、定制请求的授权规则

//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
    //首页所有人可以访问,功能页只有有权限的人可以进行访问
    //请求授权的规则
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/level1/**").hasRole("vip1")
            .antMatchers("/level2/**").hasRole("vip2")
            .antMatchers("/level3/**").hasRole("vip3");

5、测试一下:发现除了首页都进不去了!因为我们目前没有登录的角色,因为请求需要登录的角色拥有对应的权限才可以!

6、在configure()方法中加入以下配置,开启自动配置的登录功能!

//没有权限会默认到登录页面,需要开启登录的页面	/login?error 重定向到这里表示登录失败
http.formLogin();

7、测试一下:发现,没有权限的时候,会跳转到登录的页面!

 

8、查看刚才登录页的注释信息;

我们可以定义认证规则,重写configure(AuthenticationManagerBuilder auth)方法

//认证 ,springboot 2.1.x 可以直接使用~
//密码编码: PasswordEncoder
//在 spring Security 5.0+ 新增了很多的加密方法~
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

    //这些数据正常应该在数据库中读	在内存中定义,也可以在jdbc中去拿....
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
            .withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
            .and()
            .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
            .and()
            .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}

9、测试,我们可以使用这些账号登录进行测试!发现会报错!

There is no PasswordEncoder mapped for the id “null”

 

10、原因,我们要将前端传过来的密码进行某种方式加密,否则就无法登录,修改代码

//在内存中定义,也可以在jdbc中去拿....
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
            .withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
            .and()
            .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
            .and()
            .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}

11、测试,发现,登录成功,并且每个角色只能访问自己认证下的规则!搞定

权限控制和注销

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

//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
   //....
   //开启自动配置的注销的功能
      // /logout 注销请求
   http.logout();
}

2、我们在前端,增加一个注销的按钮,index.html 导航栏中

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

3、我们可以去测试一下,登录成功后点击注销,发现注销完毕会跳转到登录页面!

4、但是,我们想让他注销成功后,依旧可以跳转到首页,该怎么处理呢?

// .logoutSuccessUrl("/"); 注销成功来到首页
http.logout().logoutSuccessUrl("/");

5、测试,注销完毕后,发现跳转到首页OK

6、我们现在又来一个需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如kuangshen这个用户,它只有 vip2,vip3功能,那么登录则只显示这两个功能,而vip1的功能菜单不显示!这个就是真实的网站情况了!该如何做呢?

我们需要结合thymeleaf中的一些功能

sec:authorize="isAuthenticated()":是否认证登录!来显示不同的页面

Maven依赖:

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

7、修改我们的 前端页面

  1. 导入命名空间

<!--这里可以导入4也可以使用5-->
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"

修改导航栏,增加认证判断

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

                <div sec:authorize="isAuthenticated()">
                    <a class="item">
                        用户名:<span sec:authentication="principal.username"></span>
                        <!--角色:<span sec:authentication="principal.getAuthorities()"></span>-->
                        角色:<span sec:authentication="principal.authorities"></span>
                    </a>
                    <!--如果登录,用户名,注销-->
                    <a class="item" th:href="@{/logout}">
                        <i class="sign-out icon"></i> 注销
                    </a>
                </div>

8、重启测试,我们可以登录试试看,登录成功后确实,显示了我们想要的页面;

9、如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭csrf功能;我们试试:在 配置中增加 http.csrf().disable();

http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求

10、我们继续将下面的角色功能块认证完成!

<div class="column" sec:authorize="hasRole('vip1')">
                <div class="ui raised segment">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 1</h5>
                            <hr>
                            <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                            <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                            <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="column">
                <div class="ui raised segment" sec:authorize="hasRole('vip2')">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 2</h5>
                            <hr>
                            <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                            <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                            <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="column">
                <div class="ui raised segment" sec:authorize="hasRole('vip3')">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 3</h5>
                            <hr>
                            <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                            <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                            <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

11、测试一下!

12、权限控制和注销搞定!

记住我

现在的情况,我们只要登录之后,关闭浏览器,再登录,就会让我们重新登录,但是很多网站的情况,就是有一个记住密码的功能,这个该如何实现呢?很简单

1、开启记住我功能

//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//。。。。。。。。。。。
   //记住我
   http.rememberMe();
}

2、我们再次启动项目测试一下,发现登录页多了一个记住我功能,我们登录之后关闭 浏览器,然后重新打开浏览器访问,发现用户依旧存在!

思考:如何实现的呢?其实非常简单

我们可以查看浏览器的cookie

 

3、我们点击注销的时候,可以发现,spring security 帮我们自动删除了这个 cookie

4、结论:登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了。如果点击注销,则会删除这个cookie,具体的原理我们在JavaWeb阶段都讲过了,这里就不在多说了!

 

定制登录页

现在这个登录页面都是spring security 默认的,怎么样可以使用我们自己写的Login界面呢?

1、在刚才的登录页配置后面指定 loginpage

http.formLogin().loginPage("/toLogin");

2、然后前端也需要指向我们自己定义的 login请求

<a class="item" th:href="@{/toLogin}">
   <i class="address card icon"></i> 登录
</a>

3、我们登录,需要将这些信息发送到哪里,我们也需要配置,login.html 配置提交请求及方式,方式必须为post:

在 loginPage()源码中的注释上有写明:

 

<form th:action="@{/login}" method="post">
   <div class="field">
       <label>Username</label>
       <div class="ui left icon input">
           <input type="text" placeholder="Username" name="username">
           <i class="user icon"></i>
       </div>
   </div>
   <div class="field">
       <label>Password</label>
       <div class="ui left icon input">
           <input type="password" name="password">
           <i class="lock icon"></i>
       </div>
   </div>
   <input type="submit" class="ui blue submit button"/>
</form>

4、这个请求提交上来,我们还需要验证处理,怎么做呢?我们可以查看formLogin()方法的源码!我们配置接收登录的用户名和密码的参数!

http.formLogin()
  .usernameParameter("username")
  .passwordParameter("password")
  .loginPage("/toLogin")
  .loginProcessingUrl("/login"); // 登陆表单提交请求

5、在登录页增加记住我的多选框

<input type="checkbox" name="remember"> 记住我

6、后端验证处理!

//开启记住我功能 :cookie,默认保存时间是两周
http.rememberMe().rememberMeParameter("remember");

7、测试,OK!

 

完整配置代码

package com.kuang.config;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   //定制请求的授权规则
   @Override
   protected void configure(HttpSecurity http) throws Exception {

       http.authorizeRequests().antMatchers("/").permitAll()
      .antMatchers("/level1/**").hasRole("vip1")
      .antMatchers("/level2/**").hasRole("vip2")
      .antMatchers("/level3/**").hasRole("vip3");


       //开启自动配置的登录功能:如果没有权限,就会跳转到登录页面!
           // /login 请求来到登录页
           // /login?error 重定向到这里表示登录失败
       http.formLogin()
          .usernameParameter("username")
          .passwordParameter("password")
          .loginPage("/toLogin")
          .loginProcessingUrl("/login"); // 登陆表单提交请求

       //开启自动配置的注销的功能
           // /logout 注销请求
           // .logoutSuccessUrl("/"); 注销成功来到首页

       http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
       http.logout().logoutSuccessUrl("/");

       //记住我
       http.rememberMe().rememberMeParameter("remember");
  }

   //定义认证规则
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       //在内存中定义,也可以在jdbc中去拿....
       //Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
       //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
       //spring security 官方推荐的是使用bcrypt加密方式。

       auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
              .withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
              .and()
              .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
              .and()
              .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
  }
}

拦截器原理

1、根据当前请求,找到HandlerExecutionChain对应处理请求的handler以及handler的所有拦截器

2、先来顺序执行 所有拦截器的preHandler方法

  • 如果当前拦截器preHandler返回为true。则执行下一个拦截器的preHandler

  • 如果拦截器返回为false。直接 倒叙执行所有已经执行了的拦截器的 afterCompletion!

3、如歌任何一个拦截器返回false,直接跳出不执行,目标方法

4、所有拦截器都返回true,返回目标方法

5、倒叙执行所有拦截器的postHandler 方法。

6、前面的步骤有任何异常都会直接触发 afterCompletion

7、页面成功渲染完成之后,也会倒叙触发 afterCompletion

12、Shiro

1、Shiro简介

1.1、Shiro 是什么?

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

  • Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。

  • Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存等。

 

1.2、功能?

 

  1. Authentication:身份认证/登录,验证用户是不是拥有相应的身份

  2. Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限

  3. Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境,也可以是Web 环境的

  4. Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储

  5. Web Support:Web 支持,可以非常容易的集成到Web 环境

  6. Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率

  7. Concurrency:Shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去

  8. Testing:提供测试支持

  9. Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问

  10. Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了

1.3、Shiro架构(外部)

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

 

  • Subject:应用代码直接交互的对象是Subject,也就是说Shiro的对外API 核心就是Subject。Subject 代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;与Subject 的所有交互都会委托给SecurityManager;Subject 其实是一个门面,SecurityManager才是实际的执行者

  • SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且其管理着所有Subject;可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC中DispatcherServlet的角色

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

  • 也就是说对于我们而言,最简单的一个Shiro应用:

    • 应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;

    • 我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入

1.4、Shiro架构(内部)

 

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

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

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

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

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

  6. SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);

  7. SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;

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

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

 

1.5、认证流程

用户提交身份信息凭证信息封装成令牌交由安全管理器认证

2、快速入门

2.1、拷贝案例

  1. 按照官网提示找到快速入门案例GitHub,从GitHub 的文件中可以看出这个快速入门案例是一个 Maven 项目

  2. 新建一个 Maven 工程,删除其 src 目录,将其作为父工程

  3. 在父工程中新建一个 Maven 模块

  4. 复制快速入门案例 pom.xml 文件中的依赖 (版本号自选)

<dependencies>
    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.8.0</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.slf4j/jcl-over-slf4j -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>2.0.0-alpha5</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>2.0.0-alpha5</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/log4j/log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
</dependencies>
  1. 把快速入门案例中的 resource 下的log4j.properties 复制下来

  2. 复制一下 shiro.ini 文件

  3. 复制一下 Quickstart.java 文件,如果有导包的错误,把那两个错误的包删掉,就会自动导对的包了,快速入门案例中用的方法过时了

  4. 运行 Quickstart.java,得到结果

 

2.2、分析案例

  1. 通过 SecurityUtils 获取当前执行的用户 Subject

    Subject currentUser = SecurityUtils.getSubject();
  2. 通过 当前用户拿到 Session

    Session session = currentUser.getSession();
  3. 用 Session 存值取值

    session.setAttribute("someKey", "aValue");
    String value = (String) session.getAttribute("someKey");
  4. 判断用户是否被认证

    currentUser.isAuthenticated()
  5. 执行登录操作

    currentUser.login(token);
  6. 打印其标识主体

    currentUser.getPrincipal()
  7. 注销

    currentUser.logout();
  8. 总览

    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) {
            // 工厂模式,通过shiro.ini 配置文件中的信息,生成一个工厂实例
            Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
            SecurityManager securityManager = factory.getInstance();
            SecurityUtils.setSecurityManager(securityManager);
    
            // 通过SecurityUtils 获取当前执行的用户Subject:
            Subject currentUser = SecurityUtils.getSubject();
    
            //通过当前用户拿到Shiro的Session 可以脱离web存值取值
            Session session = currentUser.getSession();
            // 使用session存值取值
            session.setAttribute("someKey", "aValue");
            String value = (String) session.getAttribute("someKey");
            if (value.equals("aValue")) {
                log.info("Retrieved the correct value! [" + value + "]");
            }
    
            // 让我们登录当前用户,以便我们可以检查角色和权限:
            // 这里和SpringSecurity 使用了类似的代码,判断用户是否被认证
            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 (AuthenticationException ae) {
                    //意外情况?错误?
                }
            }
    
            //currentUser些用法
            //打印其标识主体(在这种情况下,为用户名) :
            log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
    
            // 检查角色是否存在
            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);
        }
    }

 

3、环境搭建

3.1. 编写配置文件

  1. 在刚刚的父项目中新建一个 springboot 模块

  2. 导入 SpringBoot 和 Shiro 整合包的依赖

<!--SpringBoot 和 Shiro 整合包-->
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.8.0</version>
</dependency>
<!--
    下面是编写配置文件
    Shiro 三大要素
    subject -> ShiroFilterFactoryBean
    securityManager -> DefaultWebSecurityManager
    realm
    实际操作中对象创建的顺序 : realm -> securityManager -> subject
-->
  1. 编写自定义的 realm ,需要继承 AuthorizingRealm

//自定义的 Realm
public class UserRealm extends AuthorizingRealm {
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //打印一个提示
        System.out.println("执行了授权方法");
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //打印一个提示
        System.out.println("执行了认证方法");
        return null;
    }
}
  1. 新建一个 ShiroConfig配置文件

    1. 创建 Realm

    //3. realm
    //让 spring 托管自定义的 realm 类
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }
    1. 创建 securityManager

    //2. securityManager -> DefaultWebSecurityManager
    // @Qualifier("userRealm") 指定 Bean 的名字为 userRealm
    // spring 默认的 BeanName 就是方法名
    // name 属性 指定 BeanName
    @Bean(name = "SecurityManager")
    public DefaultWebSecurityManager getDefaultWebSecurity(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //需要关联自定义的 Realm,通过参数把 Realm 对象传递过来
        securityManager.setRealm(userRealm);
        return securityManager;
    }
    1. 创建 subject

    //1. subject -> ShiroFilterFactoryBean
    // @Qualifier("securityManager") 指定 Bean 的名字为 securityManager
    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean subject = new ShiroFilterFactoryBean();
        //设置安全管理器
        //需要关联 securityManager ,通过参数把 securityManager 对象传递过来
        subject.setSecurityManager(securityManager);
        return subject;
    }

     

3.2、搭建简单测试环境

新建首页

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

    <hr>
    <a th:href="@{/user/add}">add</a>   | <a th:href="@{/user/update}">update</a>
</div>
</body>
</html>
  1. 新建增加用户页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>add</h1>
</body>
</html>
  1. 新建修改用户页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>update</h1>
</body>
</html>
  1. 编写对应的 Controller

@Controller
public class MyController {

    @RequestMapping({"/","/index"})
    public String toIndex(Model model) {
        model.addAttribute("msg","hello,Shiro");
        return "index";
    }
    
    @RequestMapping("/user/add")
    public String add() {
        return "user/add";
    }

    @RequestMapping("/user/update")
    public String update() {
        return "user/update";
	}
}
  1. 运行结果

 

4、使用Shiro实现功能

4.1、Shiro实现登录拦截

  1. 在ShiroConfig中的getShiroFilterFactoryBean方法中添加配置

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

 

添加拦截成功页面

  1. 登录页面login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<h1>登录</h1>
<hr>

<form action="">
    <p>用户名:<input type="text" name="username"></p>
    <p>密码:<input type="text" name="password"></p>
    <p>密码:<input type="submit"></p>
</form>
</body>
</html>
  1. 在MyController中添加

@RequestMapping("/toLogin")
public String toLogin() {
    return "login";
}
  1. 在ShiroConfig中的getShiroFilterFactoryBean方法中添加配置

//设置登录的请求
bean.setLoginUrl("/toLogin");
  1. 拦截成功页面

 

4.2、Shiro实现用户认证

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

}
  1. login.html的修改

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<h1>登录</h1>
<hr>

<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>
  1. 用户认证编写UserRealm中的认证(doGetAuthenticationInfo)

//认证
@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,"");
}

4.3、Shiro实现用户注销

在控制器中添加一个退出登录的方法

//退出登录
@RequestMapping("/logout")
public String logout(){
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    return "login";
}

5、整合Mybatis

5.1、导入依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

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

<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.8</version>
</dependency>
<!--引入mybatis,这是MyBatis官方提供的适配spring Boot的,而不是spring Boot自己的-->
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

5.2、配置文件application.yml的编写

spring:
  datasource:
    username: root
    password: "123456"
    # 如果时区报错了,就增加一个时区的配置就OK 了 serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

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

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

5.3、User类的编写

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

5.4、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" parameterType="String" resultType="User">
            select * from mybatis.user where name=#{name};
    </select>
</mapper>

5.5、UserService接口实现

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

5.6、UserServiceImpl业务逻辑

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;
    @Override
    public User queryUserByName(String name) {
        return userMapper.queryUserByName(name);
    }
}

5.7、测试环境

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

5.8、UserRealm连接真实数据库

@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、MD5盐值加密
    // 密码认证,shiro做
    return new SimpleAuthenticationInfo("",user.getPwd(),"");
}

6、授权整合Thymeleaf + Shiro

6.1、授权

6.1、添加认证代码

ShiroConfig中的getShiroFilterFactoryBean方法

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

6.2、添加未授权页面

  1. MyController

@RequestMapping("/noauto")
@ResponseBody
public String unauthorized() {
    return "未经授权,无法访问此页面";
}
  1. ShiroConfig中的getShiroFilterFactoryBean方法中添加

//未授权页面
bean.setUnauthorizedUrl("/noauth");

6.3、实现用户授权

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    //打印一个提示
    System.out.println("执行了授权方法");
    // 授权
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    // 拿到当前登录的对象
    Subject subject = SecurityUtils.getSubject();
    // 拿到User对象
    User currentUser = (User) subject.getPrincipal();
    // 设置当前用户权限
    info.addStringPermission(currentUser.getPerms());
    return info;
}

7、整合Thymeleaf

7.1、导入依赖

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

7.2、整合ShiroDialect

// 整合ShiroDialect:用来整合shiro-thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
    return new ShiroDialect();
}

7.3、index.html页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <h1>首页</h1>
    <p th:text="${msg}"></p>
    <div shiro:notAuthenticated>
        <a th:href="@{/toLogin}">登录</a>
    </div>
    
    <!--<div th:if="${session.loginUser==null}">
        <a th:href="@{/toLogin}">login</a>
    </div>-->
    <div shiro:authenticated>
        <a th:href="@{/logout}">logout</a>
    </div>
    <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>

 

13、Swagger

1、项目集成swagger

 

学习目标:

  • 了解Swagger的概念及作用

  • 掌握在项目中集成Swagger自动生成API文档

2、swagger简介

前后端分离

  • 前端 -> 前端控制层、视图层

  • 后端 -> 后端控制层、服务层、数据访问层

  • 前后端通过API进行交互

  • 前后端相对独立且松耦合

产生的问题

  • 前后端集成,前端或者后端无法做到“及时协商,尽早解决”,最终导致问题集中爆发

解决方案

  • 首先定义schema [ 计划的提纲 ],并实时跟踪最新的API,降低集成风险

Swagger

  • 号称世界上最流行的API框架

  • Restful Api 文档在线自动生成器 => API 文档 与API 定义同步更新

  • 直接运行,在线测试API

  • 支持多种语言 (如:Java,PHP等)

  • 官网:https://swagger.io/

 

3、springboot集成 swagger

SpringBoot集成Swagger => springfox,两个jar包

  • Springfox-swagger2

  • swagger-springmvc

使用Swagger

要求:jdk 1.8 + 否则swagger2无法运行

步骤:

1、新建一个SpringBoot-web项目

2、添加Maven依赖

<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
<dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger2</artifactId>
   <version>2.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger-ui</artifactId>
   <version>2.9.2</version>
</dependency>

3、编写HelloController,测试确保运行成功!

4、要使用Swagger,我们需要编写一个配置类-SwaggerConfig来配置 Swagger

@Configuration //配置类
@EnableSwagger2// 开启Swagger2的自动配置
public class SwaggerConfig {  

}

5、访问测试 :http://localhost:8080/swagger-ui.html ,可以看到swagger的界面;

 

在这里如果我们遇到了这个问题Failed to start bean ‘documentationPluginsBootstrapper‘; nested exception is空指针:那么就请降低springboot 的版本,然后重新reload一下maven既可解决!

4、配置swagger

1、Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger。

@Bean //配置docket以配置Swagger具体参数
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2);
}

2、可以通过apiInfo()属性配置文档信息

//配置文档信息
private ApiInfo apiInfo() {
   Contact contact = new Contact("联系人名字", "http://xxx.xxx.com/联系人访问链接", "联系人邮箱");
   return new ApiInfo(
           "Swagger学习", // 标题
           "学习演示如何配置Swagger", // 描述
           "v1.0", // 版本
           "http://terms.service.url/组织链接", // 组织链接
           contact, // 联系人信息
           "Apach 2.0 许可", // 许可
           "许可链接", // 许可连接
           new ArrayList<>()// 扩展
  );
}

3、Docket 实例关联上 apiInfo()

@Bean
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}

4、重启项目,访问测试 http://localhost:8080/swagger-ui.html 看下效果;

 

5、配置扫描接口

@Bean
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
      .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
      .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
      .build();
}

2、重启项目测试,由于我们配置根据包的路径扫描接口,所以我们只能看到一个类

3、除了通过包路径配置扫描接口外,还可以通过配置其他方式扫描接口,这里注释一下所有的配置方式:

any() // 扫描所有,项目中的所有接口都会被扫描到
none() // 不扫描接口
// 通过方法上的注解扫描,如withMethodAnnotation(GetMapping.class)只扫描get请求
withMethodAnnotation(final Class<? extends Annotation> annotation)
// 通过类上的注解扫描,如.withClassAnnotation(Controller.class)只扫描有controller注解的类中的接口
withClassAnnotation(final Class<? extends Annotation> annotation)
basePackage(final String basePackage) // 根据包路径扫描接口

4、除此之外,我们还可以配置接口扫描过滤:

@Bean
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
      .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
      .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
       // 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口
      .paths(PathSelectors.ant("/kuang/**"))
      .build();
}

5、这里的可选值还有

any() // 任何请求都扫描
none() // 任何请求都不扫描
regex(final String pathRegex) // 通过正则表达式控制
ant(final String antPattern) // 通过ant()控制

6、配置swagger开关

1、通过enable()方法配置是否启用swagger,如果是false,swagger将不能在浏览器中访问了

@Bean
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
      .enable(false) //配置是否启用Swagger,如果是false,在浏览器将无法访问
      .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
      .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
       // 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口
      .paths(PathSelectors.ant("/kuang/**"))
      .build();
}

2、如何动态配置当项目处于test、dev环境时显示swagger,处于prod时不显示?

@Bean
public Docket docket(Environment environment) {
   // 设置要显示swagger的环境
   Profiles of = Profiles.of("dev", "test");
   // 判断当前是否处于该环境
   // 通过 enable() 接收此参数判断是否要显示
   boolean b = environment.acceptsProfiles(of);
   
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
      .enable(b) //配置是否启用Swagger,如果是false,在浏览器将无法访问
      .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
      .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
       // 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口
      .paths(PathSelectors.ant("/kuang/**"))
      .build();
}

3、可以在项目中增加一个dev的配置文件查看效果!

 

7、配置API分组

 

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

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

2、重启项目查看分组

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

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

4、重启项目查看即可

 

8、实体配置

1、新建一个实体类

@ApiModel("用户实体类")
public class User {
   @ApiModelProperty("用户名")
   public String username;
   @ApiModelProperty("密码")
   public String password;
}

2、只要这个实体在请求接口的返回值上(即使是泛型),都能映射到实体项中:

@RequestMapping("/getUser")
public User getUser(){
   return new User();
}

3、重启查看测试

 

注:并不是因为@ApiModel这个注解让实体显示在这里了,而是只要出现在接口方法的返回值上的实体都会显示在这里,而@ApiModel和@ApiModelProperty这两个注解只是为实体添加注释的。

@ApiModel为类添加注释

@ApiModelProperty为类属性添加注释

 

9、常用注解

Swagger的所有注解定义在io.swagger.annotations包下

下面列一些经常用到的,未列举出来的可以另行查阅说明:

Swagger注解 简单说明
@Api(tags = "xxx模块说明") 作用在模块类上
@ApiOperation("xxx接口说明") 作用在接口方法上
@ApiModel("xxxPOJO说明") 作用在模型类上:如VO、BO
@ApiModelProperty(value = "xxx属性说明",hidden = true) 作用在类方法和属性上,hidden设置为true可以隐藏该属性
@ApiParam("xxx参数说明") 作用在参数、方法和字段上,类似@ApiModelProperty

我们也可以给请求的接口配置一些注释

@ApiOperation("狂神的接口")
@PostMapping("/kuang")
@ResponseBody
public String kuang(@ApiParam("这个名字会被返回")String username){
   return username;
}

这样的话,可以给一些比较难理解的属性或者接口,增加一些配置信息,让人更容易阅读!

相较于传统的Postman或Curl方式测试接口,使用swagger简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。

Swagger是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出Word接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭Swagger,一来出于安全考虑二来也可以节省运行时内存。

 

解决token拦截swagger:

// 可能会遇到token拦截了我们的swagger请求,那么这里可以解决对于swagger的配置要求:

 //放行路径
    private static final String[] EXCLUDE_PATH_PATTERNS = {
        // Swagger
        "**/swagger-ui.html", 
        "/swagger-resources/**", 
        "/webjars/**",
        "/v2/**",
        "/swagger-ui.html/**",
        "/doc.html/**",
        "/error",
        "/favicon.ico",
        "sso/auth",
        "/csrf"
}

 

 

10、拓展:其他皮肤

我们可以导入不同的包实现不同的皮肤定义:

1、默认的 访问 http://localhost:8080/swagger-ui.html

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

2、bootstrap-ui 访问 http://localhost:8080/doc.html

<!-- 引入swagger-bootstrap-ui包 /doc.html-->
<dependency>
   <groupId>com.github.xiaoymin</groupId>
   <artifactId>swagger-bootstrap-ui</artifactId>
   <version>1.9.1</version>
</dependency>

3、Layui-ui 访问 http://localhost:8080/docs.html

<!-- 引入swagger-ui-layer包 /docs.html-->
<dependency>
   <groupId>com.github.caspar-chen</groupId>
   <artifactId>swagger-ui-layer</artifactId>
   <version>1.1.3</version>
</dependency>

4、mg-ui 访问 http://localhost:8080/document.html

<!-- 引入swagger-ui-layer包 /document.html-->
<dependency>
   <groupId>com.zyplayer</groupId>
   <artifactId>swagger-mg-ui</artifactId>
   <version>1.0.6</version>
</dependency>

14、异步、定时、邮件任务

1、异步

1、创建一个service包

2、创建一个类AsyncService

异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。

编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况;

@Service
public class AsyncService {

   public void hello(){
       try {
           Thread.sleep(3000);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       System.out.println("业务进行中....");
  }
}

3、编写controller包

4、编写AsyncController类

我们去写一个Controller测试一下

@RestController
public class AsyncController {

    @Autowired
    AsyncService asyncService;

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

5、访问http://localhost:8080/hello进行测试,3秒后出现ok,这是同步等待的情况。

问题:我们如果想让用户直接得到消息,就在后台使用多线程的方式进行处理即可,但是每次都需要自己手动去编写多线程的实现的话,太麻烦了,我们只需要用一个简单的办法,在我们的方法上加一个简单的注解即可,如下:

6、给hello方法添加@Async注解;

//告诉Spring这是一个异步方法
@Async
public void hello(){
   try {
       Thread.sleep(3000);
  } catch (InterruptedException e) {
       e.printStackTrace();
  }
   System.out.println("业务进行中....");
}

SpringBoot就会自己开一个线程池,进行调用!但是要让这个注解生效,我们还需要在主程序上添加一个注解@EnableAsync ,开启异步注解功能;

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

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

}

7、重启测试,网页瞬间响应,后台代码依旧执行!

 

2、邮件任务

邮件发送,在我们的日常开发中,也非常的多,Springboot也帮我们做了支持

  • 邮件发送需要引入spring-boot-start-mail

  • SpringBoot 自动配置MailSenderAutoConfiguration

  • 定义MailProperties内容,配置在application.yml中

  • 自动装配JavaMailSender

  • 测试邮件发送

测试:

1、引入pom依赖

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

看它引入的依赖,可以看到 jakarta.mail

<dependency>
   <groupId>com.sun.mail</groupId>
   <artifactId>jakarta.mail</artifactId>
   <version>1.6.4</version>
   <scope>compile</scope>
</dependency>

2、查看自动配置类:MailSenderAutoConfiguration

 

这个类中存在bean,JavaMailSenderImpl

 

然后我们去看下配置文件

@ConfigurationProperties(
   prefix = "spring.mail"
)
public class MailProperties {
   private static final Charset DEFAULT_CHARSET;
   private String host;
   private Integer port;
   private String username;
   private String password;
   private String protocol = "smtp";
   private Charset defaultEncoding;
   private Map<String, String> properties;
   private String jndiName;
}

3、配置文件:

spring.mail.username=24736743@qq.com
spring.mail.password=你的qq授权码
spring.mail.host=smtp.qq.com
# qq需要配置ssl
spring.mail.properties.mail.smtp.ssl.enable=true

获取授权码:在QQ邮箱中的设置->账户->开启pop3和smtp服务

 

4、Spring单元测试

@Autowired
JavaMailSenderImpl mailSender;

@Test
void contextLoads() {

    //一个简单的邮件发送
    SimpleMailMessage mailMessage = new SimpleMailMessage();
    mailMessage.setSubject("小狂神你好呀");
    mailMessage.setText("谢谢你将的一Java系列课程!");

    mailMessage.setFrom("2724964601@qq.com");
    mailMessage.setTo("2724964601@qq.com");
    mailSender.send(mailMessage);
}

@Test
void contextLoads2() throws MessagingException {

    //一个复杂的邮件发送
    MimeMessage mimeMessage = mailSender.createMimeMessage();
    //组件
    MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
    helper.setSubject(" 小狂神你好-plus ~");
    helper.setText("<p style='color:green'> 谢谢你的一系列Java课程!</p>",true);

    //附件
    helper.addAttachment("1.jpg",new File("/Users/mac/Desktop/1.jpg"));
    helper.addAttachment("1.jpg",new File("/Users/mac/Desktop/1.jpg"));

    helper.setTo("2724964601@qq.com");
    helper.setFrom("2724964601@qq.com");

    mailSender.send(mimeMessage);
}

查看邮箱,邮件接收成功!

我们只需要使用Thymeleaf进行前后端结合即可开发自己网站邮件收发功能了!

3、定时任务

项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring为我们提供了异步执行任务调度的方式,提供了两个接口。

  • TaskExecutor接口

  • TaskScheduler接口

两个注解:

  • @EnableScheduling

  • @Scheduled

cron表达式:

字段 允许值 允许的特殊字符
0-59 , - * /
0-59 , - * /
小时 0-23 , - * /
日期 1-31 , - * ? / L W C
月份 1-12 , - * /
星期 0-7或SUN-SAT 0,7是SUN , - * ? / L C #
  , - * /

 

测试步骤:

1、创建一个ScheduledService

我们里面存在一个hello方法,他需要定时执行,怎么处理呢?

@Service
public class ScheduledService {
   
   //秒   分   时     日   月   周几
   //0 * * * * MON-FRI
   //注意cron表达式的用法;
   @Scheduled(cron = "0 * * * * 0-7")
   public void hello(){
       System.out.println("hello.....");
  }
}

3、我们来详细了解下cron表达式;

http://www.bejson.com/othertools/cron/

4、常用的表达式

(1)0/2 * * * * ?   表示每2秒 执行任务
(1)0 0/2 * * * ?   表示每2分钟 执行任务
(1)0 0 2 1 * ?   表示在每月的1日的凌晨2点调整任务
(2)0 15 10 ? * MON-FRI   表示周一到周五每天上午10:15执行作业
(3)0 15 10 ? 6L 2002-2006   表示2002-2006年的每个月的最后一个星期五上午10:15执行作
(4)0 0 10,14,16 * * ?   每天上午10点,下午2点,4点
(5)0 0/30 9-17 * * ?   朝九晚五工作时间内每半小时
(6)0 0 12 ? * WED   表示每个星期三中午12点
(7)0 0 12 * * ?   每天中午12点触发
(8)0 15 10 ? * *   每天上午10:15触发
(9)0 15 10 * * ?     每天上午10:15触发
(10)0 15 10 * * ?   每天上午10:15触发
(11)0 15 10 * * ? 2005   2005年的每天上午10:15触发
(12)0 * 14 * * ?     在每天下午2点到下午2:59期间的每1分钟触发
(13)0 0/5 14 * * ?   在每天下午2点到下午2:55期间的每5分钟触发
(14)0 0/5 14,18 * * ?     在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
(15)0 0-5 14 * * ?   在每天下午2点到下午2:05期间的每1分钟触发
(16)0 10,44 14 ? 3 WED   每年三月的星期三的下午2:10和2:44触发
(17)0 15 10 ? * MON-FRI   周一至周五的上午10:15触发
(18)0 15 10 15 * ?   每月15日上午10:15触发
(19)0 15 10 L * ?   每月最后一日的上午10:15触发
(20)0 15 10 ? * 6L   每月的最后一个星期五上午10:15触发
(21)0 15 10 ? * 6L 2002-2005   2002年至2005年的每月的最后一个星期五上午10:15触发
(22)0 15 10 ? * 6#3   每月的第三个星期五上午10:15触发

15、springboot集成Redis

在这里拓展一个springboot自带的缓存:

// 使用之前导入依赖

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

// 然后在启动类上开启注解:@EnableCaching // 开启缓存!
// 然后就在我们需要使用的类上添加注解即可!例如以下: 
    @Cacheable(value = "emp" ,key = "targetClass + methodName +#p0")
    public List<NewJob> queryAll(User uid) {
        return newJobDao.findAllByUid(uid);
    }
// 缓存问题:如果我们数据库更新了,但是我们的前端缓存没有更新,数据信息不一致!
// 解决问题:@CachePut!
    @CachePut(value = "emp", key = "targetClass + #p0")
    public NewJob updata(NewJob job) {
        NewJob newJob = newJobDao.findAllById(job.getId());
        newJob.updata(job);
        return job;
    }

// 清空缓存:@CacheEvict!
 //清除一条缓存,key为要清空的数据
    @CacheEvict(value="emp",key="#id")
    public void delect(int id) {
        newJobDao.deleteAllById(id);
    }



// 详情参考博客:https://blog.csdn.net/qq_32448349/article/details/101696892

SpringBoot操作数据:是封装在Spring-data中的,jpa、jdbc、mongodb、redis

在SpringBoot2.x以后与原来使用的jedis被替换成来看lettuce,底层已经不使用jedis了

jedis:采用的直连,多个线程操作的话,不安全,要提高安全性要使用jedis pool连接池! 更像BIO模式!

lettuce:采用netty,高性能网络框架,异步请求,实例在多线程中可以共享,不存在线程不安全的情况,dubbo底层也是用netty,可以减少线程数据了,更像NIO!

 

源码分析:

@Bean
@ConditionalOnMissingBean(name = "redisTemplate")//我们可以自己定义一个RedisTemplate来替换这个默认的!
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
  //默认的RedisTemplate 没有过多的配置,redis对象都是需要序列化的!
  // 两个泛型都是object ,object的类型,我们后期使用需要强制转换<String,Object>
	RedisTemplate<Object, Object> template = new RedisTemplate<>();
	template.setConnectionFactory(redisConnectionFactory);
	return template;
}

@Bean
@ConditionalOnMissingBean//由于string 类型使redis中长最常使用的类型,所以单独提出来一个bean!
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
	return new StringRedisTemplate(redisConnectionFactory);
}

整合测试一下

1、导入依赖

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

2、配置连接

#配置redis
spring.redis.port=6379
spring.redis.host=127.0.0.1

3、测试!

@SpringBootTest
class Springboot10RedisApplicationTests {

    @Autowired
    RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        //opsForValue  操作字符串 类似string
        //opsForList 操作list 类似list
        //opsForSet
        //opsForHash
        //opsForHyperLogLog
        //opsForGeo

        //除了基本的操作,我们常用的都可以直接通过 redisTemplate 操作,比如事务,和基本的CRUD!
        //获得redis的连接对象
        //RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        //connection.flushAll();
        //connection.flushDb();
        redisTemplate.opsForValue().set("mykey","kuangshen");
        System.out.println(redisTemplate.opsForValue().get("mykey"));
    }

}

 

关于对象的保存:

 

 

关于乱码解决:

1、新建一个User实体类,后面并不implement Serializable,即不序列化。

2、我们在springboot的测试类中进行测试

    @Test
    void test2() throws JsonProcessingException {
        User user = new User();
//        String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user",user);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }

我们直接调用实体类对象,如果不进行序列化操作(implement Serializable),那么在使用redis的时候,console会直接报错

 

题外话:在实际开发中我们一般不会直接使用对象,而是json字符串。

当我们在User实体类加上序列化操作时(implement Serializable),发现console打印没有再报错,问题解决,但是redis使用命令行操作后发现:存储的数据还是乱码

127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x04name"
127.0.0.1:6379> 

那么我们要解决这个问题。

结论:Redis的对象都需要序列化serialization,默认的序列化是JDK序列化。那么就有问题了,我们需要自己设置自己的序列化对象,那我们需要编写自己的redisTemplate解决乱码

RedisConfig(新建config包,新建RedisConfig类):

package com.kuang.redisspringboot.config;
 
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
 
import java.net.UnknownHostException;
 
@Configuration
@SuppressWarnings("all")
//镇压所有警告
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
//      默认的连接配置,和源码保持一致即可
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
 
//        序列化配置
//        json序列化配置--Jackson
        Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper);
 
//       string的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
 
//       string的key和hash的key都采用string的序列化
//        value都采用Jackson的序列化
 
        //key采用string序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key采用string序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value采用Jackson序列化方式
        template.setValueSerializer(objectJackson2JsonRedisSerializer);
        //hash的value采用Jackson序列化方式
        template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
 
        return template;
    }
}

测试:

package com.haoyun.redisspringboot;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.haoyun.redisspringboot.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
 
@SpringBootTest
class RedisSpringbootApplicationTests {
    @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate redisTemplate;
 
    @Test
    void test1() throws JsonProcessingException {
        User name1 = new User("name", 3);
        String name = new ObjectMapper().writeValueAsString(name1);
        redisTemplate.opsForValue().set("key1", name);
        System.out.println(redisTemplate.opsForValue().get("key1"));
 
    }
}

测试结果:控制台打印结果正常,无乱码;redis命令行操作数据存储也无乱码,正常。

 

但是在企业开发中一般不直接以原生的编写,将常用的操作封装为RedisUtils,自己写一些工具类来使用,在工具类中应该加入一些容错操作,能抛出异常,在公司能看到一些封装的RedisUtils:

RedisUtils:

@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }


    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }


    // ============================String=============================

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 普通缓存放入并设置时间
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 递增
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }


    /**
     * 递减
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }


    // ================================Map=================================

    /**
     * HashGet
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * HashSet 并设置时间
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }


    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }


    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }


    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }


    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */

    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将list放入缓存
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */

    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */

    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }

    }

}

测试使用RedisUtis:

@Autowired
private RedisUtil redisUtil;

@Test
public void test2(){
    redisUtil.set("name","kuangshen");
    System.out.println(redisUtil.get("name"));
}

 

16、Dubbo和Zookeeper集成

1、分布式理论

1、什么是分布式理论?

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

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

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

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

2、Dubbo文档

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

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

 

3、单一应用架构

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

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

缺点:

1、性能扩展比较难

2、协同开发问题

3、不利于升级维护

4、垂直应用架构

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

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

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

5、分布式服务架构

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

6、流动计算架构

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

2、什么是RPC?

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

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

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

PRC基本原理

步骤解析:

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

3、测试环境搭建

1、Dubbo

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

dubbo官网 : https://dubbo.apache.org/en/

1.了解Dubbo的特性

2.查看文档:https://dubbo.apache.org/en/docs/

dubbo基本概念

//imgs/architecture.png

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

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

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

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

调用关系说明

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

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

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

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

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

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

 

Dubbo环境搭建

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

什么是zookeeper呢?可以查看官方文档:https://zookeeper.apache.org/

 

2、window下安装zookeeper | Mac下安装zookeeper

1、下载zookeeper :地址: http://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.5.9/ !解压zookeeper

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

可能遇到问题:闪退 !

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

这里如果是mac用户:

  • 启动zookeeper服务端

    1)第一种方式  :默认是到bin目录下

    输入命令 ./zkServer.sh start启动命令

                 ./zkServer.sh status查看状态

                 ./zkServer.sh stop 停止命令

  • 2)第二种方式  配置环境变量

     a.打开终端输入以下命令,打开 .bash_profile 文件

     默认会要输入密码

    sudo vi ~/.bash_profile

    添加如下内容

    #zookeeper
    export ZK_HOME=/usr/local/zk/apache-zookeeper-3.6.2-bin/  --你zk的目录
    export PATH=$PATH:$ZK_HOME/bin 

     

    b)新建终端直接输入

    zkServer.sh start 就可以了

    启动客户端命令

    ./zkCli.sh -server 127.0.0.1:2181

    zkCli.sh -server localhost

3、修改zoo.cfg配置文件

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

注意几个重要位置:

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

clientPort=2181 zookeeper的端口号

修改完成后再次启动zookeeper

 

4、使用zkCli.cmd测试

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

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

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

 

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

我们再来查看一下节点

 

3、下载安装dubbo-admin

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

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

我们这里来安装一下:

1、下载dubbo-admin

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

2、解压进入目录

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

 server.port=7001
 spring.velocity.cache=false
 spring.velocity.charset=UTF-8
 spring.velocity.layout-url=/templates/default.vm
 spring.messages.fallback-to-system-locale=false
 spring.messages.basename=i18n/message
 spring.root.password=root
 spring.guest.password=guest
 ​
 dubbo.registry.address=zookeeper://127.0.0.1:2181

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

 mvn clean package -Dmaven.test.skip=true

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

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

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

【注意:zookeeper的服务一定要打开!】在这里如果我们遇到的问题解决不了,那么不要慌张可以在(gitee、github)上面找一下资源然后打开我们的idea进行maven的打包!

因为本人亲测,会有很多的问题,有的是版本问题有的是环境问题,有可能会是JDK/JRE...所以我们直接进入idea解决所有问题!

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

登录成功后,查看界面

安装完成!

SpringBoot + Dubbo + Zookeeper

框架搭建

1. 启动zookeeper !

2. IDEA创建一个空项目;

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

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

编写接口

 package com.kuang.provider.service;
 ​
 public interface TicketService {
    public String getTicket();
 }

编写实现类

 package com.kuang.provider.service;
 ​
 public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "《狂神说Java》";
  }
 }

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

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

编写service

 package com.kuang.consumer.service;
 ​
 public class UserService {
    //我们需要去拿去注册中心的服务
 }

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

服务提供者

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>

<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo</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及其依赖包,解决日志冲突,还需要剔除日志依赖;

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

2、在springboot配置文件中配置dubbo相关属性!

#当前应用名字
 dubbo.application.name=provider-server
 #注册中心地址    这里的地址可以使任何地址,尽量在Linux服务器上测试!
 dubbo.registry.address=zookeeper://127.0.0.1:2181
 #扫描指定包下服务    
 dubbo.scan.base-packages=com.kuang.provider.service

3、在service的实现类中配置服务注解,发布服务!注意导包问题

 import org.apache.dubbo.config.annotation.Service;
 import org.springframework.stereotype.Component;
 ​
 @Service //将服务发布出去
 @Component //放在容器中
 public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "《狂神说Java》";
  }
 }

逻辑理解 :应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它发布在指定的注册中心中!

服务消费者

1、导入依赖,和之前的依赖一样;

<!--dubbo-->
 <!-- Dubbo Spring Boot Starter -->
 <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>2.7.3</version>
 </dependency>
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo</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>

2、配置参数

 #当前应用名字
 dubbo.application.name=consumer-server
 #注册中心地址
 dubbo.registry.address=zookeeper://127.0.0.1:2181

3. 本来正常步骤是需要将服务提供者的接口打包,然后用pom文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同;

 

4. 完善消费者的服务类

package com.kuang.service;

import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;

@Service //放到容器中!
public class UserService {

    // 想拿到provider-server提供的票,去注册中心拿到服务
    @Reference  //引用 pom坐标,可以定义相同路径相同的接口名
    TicketService ticketService ;
    public void buyTicket(){
        String ticket = ticketService.getTicket();

        System.out.println("在注册中心拿到=>"+ticket);
    }

}

5. 测试类编写;

package com.kuang;

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

@SpringBootTest
class ConsumerServerApplicationTests {

    @Autowired
    UserService userService;

    @Test
    void contextLoads() {

        userService.buyTicket();
    }
}

启动测试

1. 开启zookeeper

2. 打开dubbo-admin实现监控【可以不用做】

3. 开启服务者

4. 消费者消费测试,结果:

监控中心 :

步骤:

前提:zookeeper服务已开启!

  1. 提供者提供服务

    1. 导入依赖

    2. 配置注册中心的地址,以及服务器发现名,以及扫描的包~

    3. 在想要被注册的服务上面,添加一个注解@Service

  2. 消费者如何消费

    1. 导入依赖

    2. 配置注册中心的地址,配置自己服务名~

    3. 从远程注入服务!@Reference

ok , 这就是SpingBoot + dubbo + zookeeper实现分布式开发的应用,其实就是一个服务拆分的思想!

现在和未来~

开发框架(架构的目的就是解耦)
   一、Spring
        IOC        AOP
            IOC :控制反转
                约泡:
                泡温泉,泡茶..... 泡友
                附近的人,打招呼。加微信,聊天,天天聊,--->约泡
                浴场(容器) :温泉,茶庄,泡友
                直接进温泉,就有人和你- -起了!
            
                原来我们都是自己一步步操作,现在交给容器了!我们需要什么就去拿就可以了
            AOP :切面(本质,动态代理)
                为了解决什么?不影响业务本来的情况下,实现动态增加功能,大量应用在日志,事务...等等方面
        Spring是一个轻量级的Java开源框架, 容器
目的:解决企业开发的复杂性问题
        Spring是春天,觉得他是春天,也十分复杂,配置文件!
        
   二、SpringBoot
        SpringBoot并不是新东西,就是Spri ng的升级版!
        新一代JavaEE的开发标准,开箱即用! ->拿过来就可以用!
        它自动帮我们配置了非常多的东西,我们拿来即用!
        
        特性:约定大于配置
随着公司体系越来越大,用户越来越多!
微服务架构--->新架构

    模块化,功能化!
    用户,支付,签到,娱乐,..... ;
    人多余多: -台服务器解决不了;在增加服务器!
横向
    假设A服务器占用98%资源,B服务器只占用了10% 。--负载均衡;
    
    将原来的整体项目,分成模块化,用户就是一个单独的项目,签到也是- -个单独的项目,项目和项目之前需要通信,如何通信?
    用户非常多,而签到十分少!给用户多点服务器 ,给签到少点服务器!  这就是引入了分布式!


三、微服务架构问题?
    分布式架构会遇到的四个核心问题?
    1.这么多服务,客户端该如何去访问?入口在哪里?
    2.这么多服务,服务之间如何进行通信?
    3.这么多服务,如何治理呢?
    4.服务挂了,怎么办?

四、解决方案:
    SpringCloud,是一套生态,就是来解决以上分布式架构的4个问题
    想使用SpringCloud,必须要掌握SpringBoot,因为SpringCloud是基于SpringBoot;
    1.Spring Cloud NetFlix,出来了一套解决方案! -站式解决方案。我们都可以直接去这里拿?
        Api网关,zuu1组件
        Feign --> HttpClient ---> HTTP的通信方式,同步并阻塞
        服务注册与发现,Eureka
        熔断机制,Hystrix
        
        2018年年底,NetFlix宣布无限期停止维护。生态不再维护,就会脱节。

    2. Apache Dubbo zookeeper, 第二套解决系统
        API :没有!要么找第三方组件,要么自己实现
        Dubbo是一个高性能的基于Java实现的RPC通信框架! 
        服务注册与发现,zookeeper:动物园管理者(Hadoop , Hive)
        没有:借助了Hystrix
        
        不完善,Dubbo.
    3. SpringCloud Alibaba - 站式解决方案!


五、万变不离其宗,一通百通!
    1.API网关,服务路由
    2.HTTP, RPC框架,异步调用
    3.服务注册与发现,高可用!
    4.熔断机制,服务降级

六、本质问题:网络是不可靠的


七、目前,又提出了一种方案:服务网格:下一代微服务标准,Server Mesh,代表解决方案: istio (你们未来可能需要掌握! )

 

整体思维导图:

 参考:https://www.processon.com/view/60642c56079129148ce8848d#map

扩展功能

文件上传原理:

文件上传自动配置类 :MultipartAutoConfiguration-MultipartProperties

  • 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】

  • 原理步骤

    • 1、请求进来使用文件上擦混解析器判断(isMultipart)并封装(resolverMultipart,返回MultipartHttpServletRequest)文件上传请求

    • 2、参数解析器来解析请求中的文件内容封装成MultipartFile

    • 3、将request中文件信息封装为一个Map:MultipartMap<String,MultipartFile>

FileCopyUtils,实现文件流的拷贝!

SpringBoot解决跨域问题

 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.cors.CorsConfiguration;
 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 import org.springframework.web.filter.CorsFilter;
 ​
 @Configuration
 public class CorsConfig {
 ​
     // 当前跨域请求最大有效时长。这里默认1天
     private static final long MAX_AGE = 24 * 60 * 60;
 ​
     @Bean
     public CorsFilter corsFilter() {
         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
         CorsConfiguration corsConfiguration = new CorsConfiguration();
         corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
         corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
         corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
         corsConfiguration.setMaxAge(MAX_AGE);
         source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置
         return new CorsFilter(source);
     }
 }

 

 

 
 
posted @   nakano_may  阅读(176)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示