SpringBoot2(尚硅谷同步笔记)

一、基础入门

1.1 为什么要用SpringBoot2?

SpringBoot能快速创建出生产级别的Spring应用

1.1.1 SpringBoot优点:

创建独立Spring应用

内嵌web服务器

自动starter依赖,简化构建配置

自动配置Spring以及第三方功能

提供生产级别的监控、健康检查以及外部化配置

无代码生成、无需编写XML

1.1.2 SpringBoot缺点

人称版本帝,迭代快,需要时刻关注变化

封装太深,内部原理复杂,不容易精通

1.1.3 时代背景

微服务

        微服务是一种架构风格

        一个应用拆分为一组小型服务

        每个服务运行在自己的进程内,也就是可独立部署和升级

        服务之间使用轻量HTTP交互

        服务围绕业务功能拆分

        可以由全自动部署机制独立部署

        去中心化、服务自治。服务可以使用不同的语言、不同的存储技术

分布式、云原生……

1.2 HelloWorld案例

①如果想使用springboot进行开发,需要将以下父工程导入到pom文件中


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.6</version>
    </parent>

②添加web的场景启动器

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

③编写主程序类

@SpringBootApplication
public class MainApplication {

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

}

④编写响应逻辑

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String handle01(){
        return "Hello SpringBoot2";
    }

}

⑤运行主类,输入地址

SpringBoot快速构建部署:

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

把项目打成jar包,直接在目标服务器执行

1.3 了解自动配置原理

1.3.1 依赖管理

引入依赖

无需关注版本号,自动版本仲裁

可以修改版本号

        ①查看spring-boot-dependencies里面规定当前依赖的版本用的key,比如mysql.version

        ②在当前项目里面重写配置

依赖管理:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.6</version>
    </parent>

它的父项目:几乎声明了所有开发中常用的依赖版本号

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

1.3.2 starter场景启动器

开发导入starter场景启动器

spring-boot-starter-*:*就代表某种场景

只要引入starter,这个场景的所有常规需要的依赖我们都会自动引入

一般来说*-spring-boot-stater为第三方为我们提供的简化开发的场景启动器

所有场景启动器最底层的依赖如下:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      <version>2.7.6</version>
      <scope>compile</scope>
    </dependency>

1.3.3 自动配置

自动配好Tomcat

        引入Tomcat依赖

        配置Tmocat

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <version>2.7.6</version>
      <scope>compile</scope>
    </dependency>

自动配好SpringMVC

        引入SpringMVC开发的全套组件

        自动配好了SpringMVC常用组件(功能)

自动配好Web常见功能:如字符编码问题

        SpringBoot帮我们配置好了所有web开发的常见场景

默认的包结构

        主程序所在包以及其下面所有子包里面的组件都会被默认扫描进来

        无需包扫描配置

        如果想要改变扫描路径,可以通过scanBasePackages配置

@SpringBootApplication(scanBasePackages="com.atguigu")

        也可以通过注解@ComponentScan配置,但是会和@SpringBootApplication注解发生冲突,后续会讲解 解决方案

        所有配置都拥有默认值

                默认配置最终都会映射到MultipartProperties上

                配置文件的值最终会绑定到某个类上,这个类会在容器中创建对象

        按需加载所有自动配置项

                非常多的starter

                引入了哪些场景,这个场景的自动配置才会开启

                SpringBoot所有的自动配置功能都在spring-boot-autoconfigure包里

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-autoconfigure</artifactId>
      <version>2.7.6</version>
      <scope>compile</scope>
    </dependency>

二、底层注解

2.1 @Configuration

@Configuration告诉SpringBoot这是一个配置类

配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的

配置类本身也是组件

proxyBeanMethods:代理bean方法

        设置为true,@Bean 外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例

Full、Lite配置:

        如果@Configuration(proxyBeanMethods=true):代理对象调用方法。

                SpringBoot总会检查这个组件是否在容器中,并保持每个@Bean方法被调用多少次返回的组件都是单实例(组件之间有依赖关系)

        如果@Configuration(proxyBeanMethods=false):解决组件依赖问题,不会检查这个组件是否为单实例,每个@Bean方法被调用多少次返回的组件都是新创建的

加速容器启动过程,减少判断

组件依赖必须使用Full模式,其他的一般采用Lite模式

package com.atguigu.boot.config;

import com.atguigu.boot.bean.Pet;
import com.atguigu.boot.bean.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {

    @Bean //给容器中添加组件,以方法名作为组件的id。
          //返回类型就是组件类型,返回的值就是组件在容器中的实例
    public User user01(){
        return new User("zhangsan",18);
    }

    @Bean("tom")
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }


}

检验注册组件成功:

@SpringBootApplication(scanBasePackages="com.atguigu")
public class MainApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext ioc = SpringApplication.run(MainApplication.class, args);
        String[] names = ioc.getBeanDefinitionNames();
        for(String name:names){
            System.out.println(name);
        }

    }

@Bean @Component @Controller @Service @Repository @ComponentScan

以前讲过,略过不讲

2.2 @Import

给容器中自动创建出这两个类型的组件,默认组件名字就是组件的全类名

@Import({User.class,DBHelper.class})

2.3 @Conditional

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

    @Bean("tom")
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }

    @ConditionalOnBean(name="tom")
    @Bean
    public User user01(){
        return new User("zhangsan",18);
    }

实现@Conditional的接口:

2.4 @ImportResource

导入Spring的配置文件

@ImportResource("classpath:beans.xml")

2.5 @ConfigurationProperties 配置绑定

只有在容器中的组件才会拥有SpringBoot提供的强大功能

        因此使用@ConfigurationProperties注解之前要先使用@Component

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {
    private String brand;
    private Integer price;
}

application.properties

mycar.brand=byd
mycar.price=100000a

2.6 EnableConfigurationProperties

1.开启Car属性配置绑定功能

2.把这个组件(Car)自动注册到容器中(一般针对第三方组件)

@EnableConfigurationProperties(Car.class)

三、自动配置【源码分析】

3.1 自动包规则原理

@SpringBootApplication相当于:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan

@SpringBootConfiguration

        其实就是一个@Configuration代表当前是一个配置类(核心配置类)

 @ComponentScan

@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)

        有两个自定义的扫描器,指定扫描哪些包

@EnableAutoConfiguration

@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {}

@EnableAutoConfiguration是@AutoConfigurationPackage和@Import的合成

        @AutoConfigurationPackage:底层通过@Import引入Register类,给容器导入一个组件

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

底层Register类:利用Reriigster给容器导入一系列组件

    static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        Registrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
        }

        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
        }
    }

对于方法的形参metadata:AnnotationMetadata即注解的元信息,代表注解标注在哪里、每一个属性值都是什么

可以验证introspectedClass为MainApplication,即注解 @AutoConfigurationPackage是标在MainApplication上的

 这一行负责得到根据注解元信息得到包名的数组,传递给register方法,统一注册(统一注册包)

            AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));

总的来说,@AutoConfigurationPackage利用Register给容器中导入一系列组件,然后将指定的一个包下的所有组件导入进来

自动包规则原理小结:

3.2 初始加载自动配置类

对于@EnableAutoConfiguration组合注解:

@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})

分析引入的AutoConfigurationImportSelector:

        ①利用getAutoConfigurationEntry(annotationMetadata)给容器中批量导入组件

   public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }

进入getAutoConfigurationEntry方法查看:

        ②对应的方法getCandidateConfigurations()可以获取所有的configurations,默认导入到容器中

            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);

③利用工厂加载得到所有的组件

Map<String, List<String>> loadSpringFactories(ClassLoader classLoader)

④从META-INF/spring.factories位置来加载一个文件

        默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件

                Enumeration urls = classLoader.getResources("META-INF/spring.factories");

在依赖spring-boot-autoconfigure-2.7.6.jar 包中,有META-INF/spring.factories,里面写死了spring-boot一启动就要给容器加载的所有配置类

条件装配,按需加载:

虽然144个场景的所有自动配置启动的时候默认全部加载,但是最终会按需配置

底层有@Conditional对应的限制,比如:

    @ConditionalOnClass({Advice.class})

总结:

3.3 自动配置流程

①以AopAutoConfiguration为例

        matchIfMissing即 即使没有配置value,也认为配置好了

        由于EnableAutoConfiguration注解已经记录了所有需要加载的场景,所以prefix和name都是有的,故AopAutoConfiguration生效

@AutoConfiguration
@ConditionalOnProperty(
    prefix = "spring.aop",
    name = {"auto"},
    havingValue = "true",
    matchIfMissing = true
)
public class AopAutoConfiguration {}

AopAutoConfiguration类中有两个嵌套子类:

Advice并没有导入,所以这个类配置不生效

   @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnClass({Advice.class})
    static class AspectJAutoProxyingConfiguration {
        AspectJAutoProxyingConfiguration() {
        }

        @Configuration(
            proxyBeanMethods = false
        )
        @EnableAspectJAutoProxy(
            proxyTargetClass = true
        )
        @ConditionalOnProperty(
            prefix = "spring.aop",
            name = {"proxy-target-class"},
            havingValue = "true",
            matchIfMissing = true
        )
        static class CglibAutoProxyConfiguration {
            CglibAutoProxyConfiguration() {
            }
        }

        @Configuration(
            proxyBeanMethods = false
        )
        @EnableAspectJAutoProxy(
            proxyTargetClass = false
        )
        @ConditionalOnProperty(
            prefix = "spring.aop",
            name = {"proxy-target-class"},
            havingValue = "false"
        )
        static class JdkDynamicAutoProxyConfiguration {
            JdkDynamicAutoProxyConfiguration() {
            }
        }
    }

这个类的配置条件是@ConditionalOnMissingClass({"org.aspectj.weaver.Advice"}),我们正好没有这个类,所以AspectJAutoProxyingConfiguration不能生效

与之对应的ClassProxyingConfiguration生效

    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnMissingClass({"org.aspectj.weaver.Advice"})
    @ConditionalOnProperty(
        prefix = "spring.aop",
        name = {"proxy-target-class"},
        havingValue = "true",
        matchIfMissing = true
    )
    static class ClassProxyingConfiguration {
        ClassProxyingConfiguration() {
        }

        @Bean
        static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() {
            return (beanFactory) -> {
                if (beanFactory instanceof BeanDefinitionRegistry) {
                    BeanDefinitionRegistry registry = (BeanDefinitionRegistry)beanFactory;
                    AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
                    AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
                }

            };
        }
    }

②以CacheAutoConfiguration为例

@AutoConfiguration(
    after = {CouchbaseDataAutoConfiguration.class, HazelcastAutoConfiguration.class, HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class}
)
@ConditionalOnClass({CacheManager.class})
@ConditionalOnBean({CacheAspectSupport.class})
@ConditionalOnMissingBean(
    value = {CacheManager.class},
    name = {"cacheResolver"}
)
@EnableConfigurationProperties({CacheProperties.class})
@Import({CacheAutoConfiguration.CacheConfigurationImportSelector.class, CacheAutoConfiguration.CacheManagerEntityManagerFactoryDependsOnPostProcessor.class})
public class CacheAutoConfiguration {}

通过运行主程序,检查现在有没有CacheAspectSupport这个组件:没有,所以CacheAutoConfiguration配置不生效

@SpringBootApplication(scanBasePackages="com.atguigu")
public class MainApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
        String[] beanNamesForType = run.getBeanNamesForType(CacheAspectSupport.class);
        System.out.println("@@@     "+beanNamesForType.length);

    }
}

③以DispatcherServletAutoConfiguration为例

1.@ConditionalOnWebApplication判断当前是否为一个web应用,而且必须是原生Servlet的web应用。

        因为SpringBoot2支持两种web模式开发,一种是响应式编程,一种是原生Servlet技术栈

@AutoConfiguration(
    after = {ServletWebServerFactoryAutoConfiguration.class}
)

要求在ServletWebServerFactoryAutoConfiguration类配置完毕后再配置DispatcherServletAutoConfiguration类

@AutoConfigureOrder(-2147483648)
@AutoConfiguration(
    after = {ServletWebServerFactoryAutoConfiguration.class}
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({DispatcherServlet.class})
public class DispatcherServletAutoConfiguration {}

2.这个DispatcherServletAutoConfiguration配置生效后,再看下面的配置类:

    @Configuration(
        proxyBeanMethods = false
    )
    @Conditional({DispatcherServletAutoConfiguration.DispatcherServletRegistrationCondition.class})
    @ConditionalOnClass({ServletRegistration.class})
    @EnableConfigurationProperties({WebMvcProperties.class})
    @Import({DispatcherServletAutoConfiguration.DispatcherServletConfiguration.class})
    protected static class DispatcherServletRegistrationConfiguration {}
    

@EnableConfigurationProperties({WebMvcProperties.class})

EnableConfigurationProperties注解功能:

1.开启对应类属性配置绑定功能

2.把这个组件自动注册到容器中(一般针对第三方组件)

进入WebMvcProperties类,会把前缀为“spring.mvc”的属性与WebMvcProperties的属性一一对应绑定

@ConfigurationProperties(
    prefix = "spring.mvc"
)
public class WebMvcProperties {
    private org.springframework.validation.DefaultMessageCodesResolver.Format messageCodesResolverFormat;
    private final WebMvcProperties.Format format = new WebMvcProperties.Format();
    private boolean dispatchTraceRequest = false;
    private boolean dispatchOptionsRequest = true;
    private boolean ignoreDefaultModelOnRedirect = true;
    private boolean publishRequestHandledEvents = true;
    private boolean throwExceptionIfNoHandlerFound = false;
    private boolean logRequestDetails;
    private boolean logResolvedException = false;
    private String staticPathPattern = "/**";
    private final WebMvcProperties.Async async = new WebMvcProperties.Async();
    private final WebMvcProperties.Servlet servlet = new WebMvcProperties.Servlet();
    private final WebMvcProperties.View view = new WebMvcProperties.View();
    private final WebMvcProperties.Contentnegotiation contentnegotiation = new WebMvcProperties.Contentnegotiation();
    private final WebMvcProperties.Pathmatch pathmatch = new WebMvcProperties.Pathmatch();

………………
}

3.底层已经把dispatcherServlet配置好了,所以我们不需要手动写文件配置

   @Configuration(
        proxyBeanMethods = false
    )
    @Conditional({DispatcherServletAutoConfiguration.DefaultDispatcherServletCondition.class})
    @ConditionalOnClass({ServletRegistration.class})
    @EnableConfigurationProperties({WebMvcProperties.class})
    protected static class DispatcherServletConfiguration {
        protected DispatcherServletConfiguration() {
        }

        @Bean(
            name = {"dispatcherServlet"}
        )
        public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
            DispatcherServlet dispatcherServlet = new DispatcherServlet();
            dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
            dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
            dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
            dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
            dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
            return dispatcherServlet;
        }

4.相当于给容器中加入了文件上传解析器

        给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找

        规范化用户配置的文件上传解析器(SpringBoot从容器中找一个MultipartResolver这个类型的对象给你的resolver赋值,并返回回去)

                如果你的文件上传解析器不叫multipartResolver,因为@Bean不标注名字默认以方法名作为组件名,所以SpringBoot会找到你配置的文件上传解析器,并规范命名作为组件

        @Bean
        @ConditionalOnBean({MultipartResolver.class})
        @ConditionalOnMissingBean(
            name = {"multipartResolver"}
        )
        public MultipartResolver multipartResolver(MultipartResolver resolver) {
            return resolver;
        }
    }

④以HttpEncodingAutoConfiguration为例

@AutoConfiguration
@EnableConfigurationProperties({ServerProperties.class})
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({CharacterEncodingFilter.class})
@ConditionalOnProperty(
    prefix = "server.servlet.encoding",
    value = {"enabled"},
    matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {}

处理乱码问题:SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了,则以用户的配置优先

处理方案:

        @Bean

        @ConditionalOnMissingBean

    @Bean
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE));
        return filter;
    }

总结:

        SpringBoot先加载所有的自动配置类 xxxxAutoConfiguration

        每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值

                数据从xxxxProperties里拿,xxxxPropeties和配置文件进行了绑定

        生效的配置类就会给容器中装配很多组件

        只要容器中有这些组件,相当于就有了对应的功能

        只要用户有自己配置的组件,就以用户的优先        

 定制化配置:

        ①用户直接@Bean替换配置文件

        ②用户直接去看这个组件时获取的配置文件的什么值

xxxxAutoConfiguration--->组件--->xxxProperties里面拿值--->application.properties

四、最佳实践

4.1 SpringBoot应用如何编写

1.引入场景依赖

2.查看自动配置了哪些内容(选做)

        自己分析,引入场景对应的自动配置一般都生效了

        配置文件中debug=true开启自动配置报告。Negative(不生效)\Positive(生效)

3.可能需要修改:

        参照文档修改配置项

        自定义加入或者替换组件

        自定义器 xxxxCustomizer

4.2 Lombok简化开发

1.引入依赖

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

2.直接使用

@Data 生成已有属性的getter、setter方法

@ToString        生成toString方法

@NoArgsConstructor 无参构造器

@AllArgsConstructor 有参构造器

@EqualsAndHashCode

@Slf4j        自动注入一个log属性

@Slf4j
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String handle01(){
        log.info("请求进来了");
        return "Hello SpringBoot2";
    }

}

4.3 dev-tools

dev-tools 热更新。每次修改代码以后,不用重启项目,只需要ctrl+f9就能实时生效

引入依赖:

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

如果想要真正的热更新功能,可以使用付费插件jrebel

4.4 Spring Initializer(项目初始化向导)

 勾选需要的模块:

五、配置文件

5.1 yaml

5.1.1 简介

YAML ,非常适合用来做以数据为中心的配置文件

YAML是 “YAML Ain't Markup Language(YAML不是一种标记语言)”的递归缩写。在开发这种语言的时候,YAML的意思其实是“Yet Another Markup Language”(仍是一种标记语言)。因此,YAML也被戏称为薛定谔的标记语言

5.1.2 基本语法

key: value;kv之间有空格

大小写敏感

使用缩进表示层级关系

缩进不允许使用tab,只允许使用空格

缩进的空格数不重要,只要相同层级的元素左对齐即可

#表示注释

字符串无需加引号,如果要加,''与""表示字符串的内容,会被 转义/不转义

5.1.3 数据类型

字面量:单个的、不可再分的值。date、boolean、string、number、null

k: v

对象:键值对的集合。map、hash、set、object

行内写法:

        k: {k1: v1,k2: v2,k3: v3}

或:

k:

        k1: v1

        k2: v2

        k3: v3

数组:一组按次序排列的值。array、list、queue

行内写法:

        k: {v1,v2,v3}

或:

k:

        - v1

        - v2

        - v3

5.1.4 用法示例

@Data
@ToString
@Component
@ConfigurationProperties(prefix="person")
public class Person {
    private String userName;
    private Boolean boss;
    private Date birth;
    private Integer age;
    private Pet pet;
    private String[] interests;
    private List<String> animal;
    private Map<String,Object> score;
    private Set<Double> salaries;
    private Map<String,List<Pet>> allPets;

}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Pet {
    private String name;
    private Double weight;
}

yaml写法:

person:
  userName: zhangsan
  boss: true
  birth: 2019/12/9
  age: 18
  # interests: [篮球,足球]
  interests:
    - 篮球
    - 足球
    - rap
  animal: [阿猫,阿狗]
  #score:
   # english: 80
   # math: 90
  score: {english:80,math:90}
  salaries:
    - 9999.44
    - 9999.11
  pet:
    name: 汪汪
    weight: 65.78
  allPets:
    sick:
      - {name:阿狗,weight:34.6}
      - name: 阿猫
        weight: 88.77
      - name: 阿虫
        weight: 77.66
    healthy:
      - {name:阿花,weight:56.65}
      - {name:阿草,weight:44.55}

运行截图:

 备注:单引号会将\n作为字符串输出,双引号会将\n作为换行输出

                双引号不会转义,单引号会转义

5.2 自定义绑定的配置提示

引入依赖:

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

按照命名规则,会将小驼峰改为短横线

比如userName改为user-name

这个插件只在开发阶段使用,所以SpringBoot打包的时候应该将它排除在外

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>3.0.0</version>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-configuration-processor</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

六、Web场景

6.1 静态资源规则与定制化

6.1.1 静态资源目录

静态资源目录:

/static     

/public

/resources

/META-INF/resources

  只要静态资源放在类路径的上述四个目录下,访问的路径就是当前项目的根路径/+静态资源名

如果动态请求路径和静态资源名字相同,先处理动态请求

原理:静态映射/**

        请求进来,先去找Controller看能不能处理,不能处理的所有请求又都交给静态资源处理器。

静态资源就回去上述四个目录下寻找,能找到则返回;找不到则404

也可以自定义静态资源所在路径:

spring:
  web:
    resources:
      static-locations: classpath:/haha/

6.1.2 静态资源访问前缀

静态资源默认没有前缀,但是为了方便今后的拦截器配置(拦截所有/**请求,并放行指定前缀请求),可以给静态资源加上前缀

spring:
  mvc:
    static-path-pattern: /resources/**

6.2 welcome和favicon功能

6.2.1 欢迎页支持

静态资源路径下index.html

        可以配置静态资源路径,但是不可以配置静态资源的访问前缀,否则会导致index.html不能被默认访问

controller处理/index

6.2.2 favicon功能支持

只需要将favicon.ico 静态资源放在静态资源目录下,SpringBoot就可以自动地加载这个小图标

6.3 静态资源原理

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

SpringMVC功能的自动配置类: WebMvcAutoConfiguration(大多集中于此)

分析WebMvcAutoConfiguration类:

①兼容RESTful风格

    @Bean
    @ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
    @ConditionalOnProperty(
        prefix = "spring.mvc.hiddenmethod.filter",
        name = {"enabled"}
    )
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new OrderedHiddenHttpMethodFilter();
    }

②配置文件的相关数据和xxx进行了绑定

 @Configuration(
        proxyBeanMethods = false
    )
    @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
    @EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class})
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {}

WebMvcProperties.class跟配置文件spring.mvc进行了绑定

@ConfigurationProperties(
    prefix = "spring.mvc"
)
public class WebMvcProperties {}

WebProperties.class跟配置文件spring.web进行了绑定

@ConfigurationProperties("spring.web")
public class WebProperties {}

扩展知识:配置类只有一个有参构造器。而有参构造器所有参数的值都会从容器中确定。

参数分析:

        WebProperties webProperties :获取spring.web绑定的所有值的对象

        WebMvcProperties webMvcProperties:获取spring.mvc绑定的所有值的对象

        ListableBeanFactory beanFactory:构建ioc容器(Spring的beanFactory)

        ObjectProvider<HttpMessageConverters> messageConvertersProvider:找到所有的HttpMesageConverters

                HttpMessageConverter可以把不同类型的body转为Java对象,也可以吧Java对象转为满足要求的body,在序列化与反序列化中有非常重要的作用。

        ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider:找到资源处理器的自定义器

        ObjectProvider<DispatcherServletPath> dispatcherServletPath:相当于dispatcherServlet中处理的路径

         ObjectProvider<ServletRegistrationBean<?>> servletRegistrations:给应用注册Servlet、Filter等

        public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
            this.resourceProperties = webProperties.getResources();
            this.mvcProperties = mvcProperties;
            this.beanFactory = beanFactory;
            this.messageConvertersProvider = messageConvertersProvider;
            this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
            this.dispatcherServletPath = dispatcherServletPath;
            this.servletRegistrations = servletRegistrations;
        }

所有资源处理器的奥秘均在于此:

        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");
            } else {
                this.addResourceHandler(registry, this.mvcProperties.getWebjarsPathPattern(), "classpath:/META-INF/resources/webjars/");
                this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
                    registration.addResourceLocations(this.resourceProperties.getStaticLocations());
                    if (this.servletContext != null) {
                        ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
                        registration.addResourceLocations(new Resource[]{resource});
                    }

                });
            }
        }

        private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
            this.addResourceHandler(registry, pattern, (registration) -> {
                registration.addResourceLocations(locations);
            });
        }

        private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, Consumer<ResourceHandlerRegistration> customizer) {
            if (!registry.hasMappingForPattern(pattern)) {
                ResourceHandlerRegistration registration = registry.addResourceHandler(new String[]{pattern});
                customizer.accept(registration);
                registration.setCachePeriod(this.getSeconds(this.resourceProperties.getCache().getPeriod()));
                registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
                registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
                this.customizeResourceHandlerRegistration(registration);
            }
        }

①禁用所有静态资源

        依据代码,如果this.resourceProperties.isAddMappings()为false,则整个静态资源配置失效。所以可以通过下面yaml禁用静态资源。

spring:
  web:
    resources:
      add-mappings: false

②静态资源配置规则

1.webjar配置规则

this.addResourceHandler(registry, this.mvcProperties.getWebjarsPathPattern(), "classpath:/META-INF/resources/webjars/");

点进getWebjarsPathPattern()可以发现定义了:

      private String webjarsPathPattern = "/webjars/**";

然后步入下列函数:也就是说只要访问了/webjars/xxx,SpringBoot就会去classpath:/META-INF/resources/webjars/下寻找资源

        private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
            this.addResourceHandler(registry, pattern, (registration) -> {
                registration.addResourceLocations(locations);
            });
        }

2.静态资源配置:

if (!registry.hasMappingForPattern(pattern)) {}获取pattern为/** ,是静态资源

为静态资源配置缓存:

        resourceProperties和Spring的配置相关绑定

        resourceProperties.getCache()获取缓存策略===>静态资源可以配置缓存

比如配合一个缓存:

spring:
  web:
    resources:
      cache:
        period: 1100

点进period查看,可以发现缓存是以秒为单位的。也就是说,静态资源默认可以存多少秒,在这个时间段内不需要向服务器发送请求拿静态资源

           @DurationUnit(ChronoUnit.SECONDS)
            private Duration period;

可以单步调试得到设置的缓存时间:


 

③欢迎页的处理规则

HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。

在new WelcomePageHandlerMapping的时候,传入参this.mvcProperties.getStaticPathPattern(),就是在yml中配置的static-path-pattern

        @Bean
        public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
            WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
            welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
            welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
            return welcomePageHandlerMapping;
        }

底层WelcomePageHandlerMapping的判断条件:

        如果当前welcomePage欢迎页不为空,并且以/**为静态路径,则跳转至index.html

    WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
        if (welcomePage != null && "/**".equals(staticPathPattern)) {
            logger.info("Adding welcome page: " + welcomePage);
            this.setRootViewName("forward:index.html");
        } else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
            logger.info("Adding welcome page template: index");
            this.setRootViewName("index");
        }

    }

七、请求处理

7.1 Rest映射及源码分析

请求映射:@xxxMapping

Rest风格支持(使用HTTP请求方式来表示对资源操作)

以前:

        /getUser        获取用户

        /deleteUser        删除用户

        /editUser        修改用户

        /saveUser        保存用户

现在:

        /user       

GET-获取用户        DELETE-删除用户        PUT-修改用户        POST-保存用户

核心Filter:HiddenHttpMethodFilter

        用法:表单method=post,隐藏域_method=put

①注意:需要手动开启hiddenHttpMethodFilter的enabled属性

    @Bean
    @ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
    @ConditionalOnProperty(
        prefix = "spring.mvc.hiddenmethod.filter",
        name = {"enabled"}
    )
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new OrderedHiddenHttpMethodFilter();
    }

手动开启:(开启页面表单的Rest功能)

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true

②HiddenHttpMethodFilter底层规定:如果想要提交put或者delete请求,需要设定POST请求,并且需要设定_method

      private String methodParam = "_method";

      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        HttpServletRequest requestToUse = request;
        if ("POST".equals(request.getMethod()) && request.getAttribute("jakarta.servlet.error.exception") == null) {
            String paramValue = request.getParameter(this.methodParam);
            if (StringUtils.hasLength(paramValue)) {
                String method = paramValue.toUpperCase(Locale.ENGLISH);
                if (ALLOWED_METHODS.contains(method)) {
                    requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
                }
            }
        }

        filterChain.doFilter((ServletRequest)requestToUse, response);
    }

表单具体实现方式:

<form action="/user" method="post">
    <input type="hidden" name="_method" value="delete">
    <input type="submit" value="REST-DELETE 提交">
</form>

<form action="/user" method="post">
    <input type="hidden" name="_method" value="put"/>
    <input type="submit" value="REST-PUT 提交">
</form>

③接收并响应请求

@RestController
public class HelloController {

    @RequestMapping(value="/user",method= RequestMethod.DELETE)
    public String deleteUser(){
        return "DELETE-张三";
    }

    @RequestMapping(value="/user",method=RequestMethod.PUT)
    public String putUser(){
        return "PUT-张三";
    }

}

原理分析:(针对表单提交使用REST风格的时候)

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        HttpServletRequest requestToUse = request;
        if ("POST".equals(request.getMethod()) && request.getAttribute("jakarta.servlet.error.exception") == null) {
            String paramValue = request.getParameter(this.methodParam);
            if (StringUtils.hasLength(paramValue)) {
                String method = paramValue.toUpperCase(Locale.ENGLISH);
                if (ALLOWED_METHODS.contains(method)) {
                    requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
                }
            }
        }

        filterChain.doFilter((ServletRequest)requestToUse, response);
    }

①表单提交会带上_method=PUT

②请求过来会被HiddenHttpMethodFilter拦截

        i.请求正常,并且是POST

                获取到_method的值。

                        如果长度大于0(不为空),则将得到的请求方式toUpperCase转为大写

                        判断获取的请求是否在ALLOWED_METHODS里

                        兼容以下请求:

    static {
        ALLOWED_METHODS = List.of(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name());
    }

                        原生request(post),包装模式requestWrapper继承了原生的HttpServletRequestWrapper,重写了getMethod方法,返回的是传入的值

    private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
        private final String method;

        public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
            super(request);
            this.method = method;
        }

        public String getMethod() {
            return this.method;
        }
    }

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

Rest使用客户端工具,如postman,直接发送put、delete等方式请求,无需filter

7.2 如何改变默认的_method?

@Configuration(proxyBeanMethods = false)
public class WebConfig {

    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
        methodFilter.setMethodParam("_m");
        return methodFilter;
    }
}

7.3 请求映射原理

首先,所有的请求都会来到DispatcherServlet

DispatcherServlet继承树如下:

 在FrameworkServlet中重写了doGet等方法,而且所有重写的方法最终都是调用proccessRequest方法。

而processRequest底层调用的是一个抽象未实现的方法doService

    protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.processRequest(request, response);
    }

    protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.processRequest(request, response);
    }

    protected final void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.processRequest(request, response);
    }

    protected final void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.processRequest(request, response);
    }

doService:

    protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception;

所以我们可以推断DispatcherServlet一定重写了doService:核心实现时doDispatch

this.doDispatch(request, response);

因此可以推断,SpringMVC功能分析都从doDispatch开始

而doDispatch核心:找到具体的controller处理路径

                    mappedHandler = this.getHandler(processedRequest);

如何找到对应的handler处理请求呢?

①HandlerMapping:处理器映射

重点关注RequestMappingHandlerMapping

②RequestMappingHandlerMapping:@RequestMapping注解对应的HandlerMapping,保存了@RequestMapping和handler的映射规则

总结:所有的请求映射都在HandlerMapping中

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

        SpringBoot自动配置了默认的RequestMappingHandlerMapping

        请求进来,挨个尝试所有的HandlerMapping看是否有请求信息

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

                如果没有,就继续找下一个HandlerMapping

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

7.4 常用参数注解使用

测试所用界面:

index.html:

<h1>尚硅谷欢迎您~</h1>
<h3>基本注解测试:</h3>
<ul>
    <li> <a href="car/3/owner/lisi">@PathVariable</a></li>
    <li><a href="car">@RequestHeader</a></li>
    <li><a href="car1?name=ikun&hobby=basketball&hobby=rap">@RequestParam</a></li>
    <li><a href="car2">@CookieValue</a></li>
</ul>

<br>

<form action="/save" method="post">
    用户名: <input type="text" name="userName"><br>
    邮箱: <input type="text" name="email"><br>
    <input type="submit" value="提交">
</form>

@PathVariable

        获取路径上传递过来的参数,可以用map接收,键值固定为String

    @GetMapping("/car/{id}/owner/{username}")
    public Map<String,Object> getCar(@PathVariable("id")int id,@PathVariable("username")String username,@PathVariable Map<String,String> kv){
        Map<String,Object> map=new HashMap<>();
        map.put("id",id);
        map.put("username",username);
        map.put("kv",kv);
        return map;
    }

 @RequestHeader

    @GetMapping("/car")
    public Map<String,Object> testRequestHeader(@RequestHeader("User-Agent")String userAgent,@RequestHeader Map<String,String>headers){
        HashMap<String, Object> map = new HashMap<>();
        map.put("userAgent",userAgent);
        map.put("headers",headers);
        return map;
    
    }

@RequestParam

    @GetMapping("/car1")
    public Map<String,Object> testRequestParam(@RequestParam("name")String name, @RequestParam("hobby")List<String> hobby,@RequestParam Map<String,String> params){
        HashMap<String, Object> map = new HashMap<>();
        map.put("name",name);
        map.put("hobby",hobby);
        map.put("params",params);
        return map;
    }

@CookieValue

    @GetMapping("/car2")
    public Map<String,Object> testCookieValue(@CookieValue("username-localhost-8889")String username_localhost_8889){
        HashMap<String,Object> map=new HashMap<>();
        map.put("username-localhost-8889",username_localhost_8889);
        return map;
    }

@RequestBody

    @PostMapping("/save")
    public Map postMethod(@RequestBody String content){
        HashMap<String,Object> map=new HashMap<>();
        map.put("content",content);
        return map;
    }

 7.5 RequestAttribute

获取request请求域中所保存的属性的值


@Controller
public class RequestController {

    @GetMapping("/goto")
    public String goToPage(HttpServletRequest request){
        request.setAttribute("msg","成功了哦");
        request.setAttribute("code",200);
        return "forward:/success";  //转发到/success请求
    }

    @GetMapping("/success")
    @ResponseBody
    public Map success(@RequestAttribute("msg") String msg,
                          @RequestAttribute("code")int code,
                          HttpServletRequest request){
        Object msg1 = request.getAttribute("msg");
        Map<String,Object> map=new HashMap<>();
        map.put("requestMethod_msg",msg1);
        map.put("annotation_msg",msg);
        map.put("annotation_code",code);
        return map;
    }
}

7.6 @MatrixVariable与UrlPathHelper

/cars/{path}?aaa=vvv&ccc=ddd        被称之为queryString,查询字符串。用@RequestParam注解

/cars/{path;low=34;brand=byd,audi,yd}        被称为矩阵变量,使用@MatrixVariable注解

面试题:

做页面开发的时候,Cookie如果被禁用了,session的内容还怎么使用?

cookie-session原理:

        session.set("a",avalue)==>每个人都有一个jsessionid==>保存在cookie中,每次发请求携带==>服务器根据携带的jsessionid找到session对象,再调用session里的get方法,就能找到对应的内容

SpringBoot默认禁用了矩阵变量的功能,需要手动开启

        手动开启原理:对于路径的处理,都是使用UrlPathHelper进行解析的,而removeSemicolonContent判断是否移除分号后的内容

矩阵变量必须有url路径变量才能被解析

自定义SpringMVC内容方法:

一、不用@EnableWebMvc注解。使用@Configuration+WebMvcConfigurer自定义规则

二、声明WebMvcRegistrations,改变默认底层组件

三、使用@EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC

@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        //设置不移除分号;后的内容,这样矩阵变量才能生效
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }
}

处理请求:

    <li><a href="cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable</a></li>
    @GetMapping("/cars/{path}")
    public Map carsSell(@MatrixVariable("low")int low,@MatrixVariable("brand")List<String> brand,@PathVariable("path")String path){
        Map<String,Object> map=new HashMap<>();
        map.put("low",low);
        map.put("brand",brand);
        map.put("path",path);
        return map;
    }

 

测试不同路径相同名字的值:

    <li><a href="boss/1;age=20/2;age=10">不同路径下相同值的获取</a></li>
    @GetMapping("/boss/{bossId}/{empId}")
    public Map boss(@MatrixVariable(value="age",pathVar = "bossId")int bossAge,
                    @MatrixVariable(value="age",pathVar="empId")int empAge){
        Map<String,Object> map=new HashMap<>();
        map.put("bossAge",bossAge);
        map.put("empAge",empAge);
        return map;
    }

7.7 各种类型参数解析原理

①从HandlerMapping中找到能处理请求的Handler(Controller.method())

②为当前Handler找一个适配器HandlerAdapter

最关键的一步:

                    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());

HandlerAdapter:

0-支持方法上标注@RequestMapping

1-支持函数式编程

 匹配适配器为RequestMappingHandlerAdapter,并返回

 ③执行目标方法(官方注释:Actually invoke the handler)

//DispatherServlet类doDispatch方法                  
 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

真正执行目标方法的语句:

            mav = this.invokeHandlerMethod(request, response, handlerMethod);

参数解析器:确定将要执行的目标方法的每一个参数的值是什么

点进argumentResovers,可以发现:

        SpringMVC目标方法能写多少种参数类型,取决于参数解析器

 参数解析器的两个接口:

        ①判断当前参数是否支持解析这种参数

        ②如果支持,则调用resolveArgument

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);

    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

④返回值处理器

 ⑤真正执行目标方法

目标方法invokeAndHandle

        invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);

在invokeAndHandle方法内部执行invokeForRequest(ServletInvocableHandlerMethod类里),从而真正执行目标方法

        Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);

执行invokeForRequest方法,可以获取请求携带的参数

        Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);

得到args

⑥具体如何确定目标方法每一个参数的值

出处:InvocableHandlerMethod类getMethodArgumentValues方法

    protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        MethodParameter[] parameters = this.getMethodParameters();
        if (ObjectUtils.isEmpty(parameters)) {
            return EMPTY_ARGS;
        } else {
            Object[] args = new Object[parameters.length];

            for(int i = 0; i < parameters.length; ++i) {
                MethodParameter parameter = parameters[i];
                parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
                args[i] = findProvidedArgument(parameter, providedArgs);
                if (args[i] == null) {
                    if (!this.resolvers.supportsParameter(parameter)) {
                        throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
                    }

                    try {
                        args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
                    } catch (Exception var10) {
                        if (logger.isDebugEnabled()) {
                            String exMsg = var10.getMessage();
                            if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
                                logger.debug(formatArgumentError(parameter, exMsg));
                            }
                        }

                        throw var10;
                    }
                }
            }

            return args;
        }
    }

整个代码执行流程:

i.获取所有参数(包括类型、注解等等信息)

 ii.遍历所有参数

iii.挨个确定参数解析器是否支持这个参数的解析

    public boolean supportsParameter(MethodParameter parameter) {
        return this.getArgumentResolver(parameter) != null;
    }

底层真正地实现:

    @Nullable
    private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
        HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
        if (result == null) {
            Iterator var3 = this.argumentResolvers.iterator();

            while(var3.hasNext()) {
                HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
                if (resolver.supportsParameter(parameter)) {
                    result = resolver;
                    this.argumentResolverCache.put(parameter, resolver);
                    break;
                }
            }
        }

        return result;
    }

第一个适配器的判断语句中supportsParameter实现原理:

        if (parameter.hasParameterAnnotation(RequestParam.class)) {
        } else if (parameter.hasParameterAnnotation(RequestPart.class)) {

 遍历每一个适配器,找到可以解析这个参数的适配器:(通过supportParamter)

        if (!parameter.hasParameterAnnotation(PathVariable.class)) {

iv.开始解析这个参数的值

        a.调用resolveArgument方法

args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);

        b.进入resolveArgument方法实现

return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);

        c.通过uriTemplateVars获取路径值的模板

        Map<String, String> uriTemplateVars = (Map)request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, 0);

7.8 Servlet API参数解析原理

①适配器判断,寻找合适的适配器(ServletRequestMethodArgumentResolver)

    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> paramType = parameter.getParameterType();
        return WebRequest.class.isAssignableFrom(paramType) || ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType) || HttpSession.class.isAssignableFrom(paramType) || pushBuilder != null && pushBuilder.isAssignableFrom(paramType) || Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations() || InputStream.class.isAssignableFrom(paramType) || Reader.class.isAssignableFrom(paramType) || HttpMethod.class == paramType || Locale.class == paramType || TimeZone.class == paramType || ZoneId.class == paramType;
    }

②将解析器缓存起来

this.argumentResolverCache.put(parameter, resolver);

③调用resolveArgument

===>ServletRequestMethodArgumentResolver负责解析Servlet API

7.9 Model、Map原理

Map、Model(map、model里面的数据会被放在request的请求域中 request.setAttribute)

RedirectAttributes(重定向携带数据)

ServletResponse(原生的response)

用于测试的接口:


    @GetMapping("/params")
    public String testParam(Map<String,Object> map, Model model, HttpServletRequest request, HttpServletResponse response){
        map.put("hello1","woyaosile");
        model.addAttribute("hello2","wuwuwuwuwuwu");
        request.setAttribute("message","byebyeworld");
        Cookie cookie = new Cookie("c1","v1");
        response.addCookie(cookie);
        return "forward:/success";
    }

直接运行:===>Model、Map<String,Object>、request都是可以给request域中放数据的

原理分析:

①对于Map类型,适配器是MapMethodProcessor

适配器判断方法如下:

    public boolean supportsParameter(MethodParameter parameter) {
        return Map.class.isAssignableFrom(parameter.getParameterType()) && parameter.getParameterAnnotations().length == 0;
    }

进行参数解析:

args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);

如果参数是map,会在mavContainer中调用getModel方法

        return mavContainer.getModel();

getModel方法会返回一个ModelMap

    public ModelMap getModel() {

而这个ModelMap就是defaultMap

    private final ModelMap defaultModel = new BindingAwareModelMap();

===>如果是map类型的参数,会返回mavContainer.getModel()===>getModel方法返回一个new BindingAwareModelMap===>BindingAwareModelMap继承自ExtendedModelMap

public class ExtendedModelMap extends ModelMap implements Model {

ModelMap就是Map

public class ModelMap extends LinkedHashMap<String, Object> {

综上,BindingAwareModelMap既是Model也是Map

②Model类型 底层也是通过mavContainer.getModel()获取到值的

适配器:ModelMethodProcessor

解析参数:

args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);

resolveArgument底层实现:

    @Nullable
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure");
        return mavContainer.getModel();
    }

map和model都会把数据放到请求域中

③Map和Model为什么可以将数据放到请求域中?

目标方法执行完成:

        将所有的数据都放在ModelAndViewContainer:包含要去的页面地址View,还包含Model数据

处理返回结果(传入mavContainer,关注mavContainer有没有做什么操作):

            this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);

 处理返回值的具体实现:(处理视图)

        返回的地址称为视图,数据称为模型。

    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        if (returnValue instanceof CharSequence) {
            String viewName = returnValue.toString();
            mavContainer.setViewName(viewName);
            if (this.isRedirectViewName(viewName)) {
                mavContainer.setRedirectModelScenario(true);
            }
        } else if (returnValue != null) {
            String var10002 = returnType.getParameterType().getName();
            throw new UnsupportedOperationException("Unexpected return type: " + var10002 + " in method: " + returnType.getMethod());
        }

    }

执行方法mav=invokeHandlerMethod(request,response,handlerMethod),并返回mav

执行方法 mappedHandler.applyPostHandle(processedRequest,response,mv)

 处理派发结果:(处理最终结果)

this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);

render方法渲染页面:

            this.render(mv, request, response);

解析视图名:

            view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);

关键方法:

renderMergeOutputModel()下的方法:把暴露模型作为请求域的属性

7.10自定义对象参数

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

表单:

<h1>尚硅谷欢迎您~</h1>
<form action="/saveuser" method="post">
    姓名:<input name="userName" value="zhangsan"/><br>
    年龄:<input name="age" value="18"><br>
    生日:<input name="birth" value="2019/12/10"><br>
    宠物姓名:<input name="pet.name" value="阿猫"><br>
    宠物年龄:<input name="pet.age" value="5"><br>
    <input type="submit" value="保存">
</form>

接口不做处理,直接返回数据:

    @PostMapping("/saveuser")
    public Person saveuser(Person person){
        return person;
    }

 页面可以接收到数据:

数据绑定:页面提交的请求数据(GET/POST)都可以和对象属性进行绑定

pojo封装过程:

处理器resolver:ServletModelAttributeMethodProcessor

第一步:适配器处理请求

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

 返回调用handleInternal方法的结果:

return this.handleInternal(request, response, (HandlerMethod)handler);

第二步:调用invokeAndHandle方法

进入handleInternal方法,调用invokeHandlerMethod方法

 mav = this.invokeHandlerMethod(request, response, handlerMethod);

进入invokeHandlerMethod方法,通过invocableMethod调用invokeAndHandle方法

        invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);

第三步:确定请求参数

Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);

通过getMethodArgumentValues得到所有请求参数

        Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);

内部处理每一个参数(resolveArgument方法)

args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);

内部逻辑:依次确定每个参数的处理器

            while(var3.hasNext()) {
                HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
                if (resolver.supportsParameter(parameter)) {
                    result = resolver;
                    this.argumentResolverCache.put(parameter, resolver);
                    break;
                }
            }

对于pojo,处理器是ServletModelAttributeMethodProcessor

第四步:找到处理器以后,开始解析参数

args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);

真正拿到解析器:

        HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);

开始处理参数:

            return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);

===>resolveArgument内部逻辑:

    @Nullable
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
        if (resolver == null) {
            throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
        } else {
            return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
        }
    }

第五步:创建实例,并填入请求参数

创建一个空的Person实例

attribute = this.createAttribute(name, parameter, binderFactory, webRequest);

如果bindingResult==null,调用方法createBinder(关键),创建一个Web数据绑定器(WebDataBinder)

WebDataBinder:web数据绑定器,将请求参数的值绑定到指定的JavaBean里面

参数 attribute被封装到binder的target里面

            WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);

并且binder里面封装了124个converters

原理:要把请求的数据绑定到JavaBean里,又请求传过来的数据是HTTP,即超文本传输协议,默认认为万物皆文本,要把文本转换为String、Integer等类型就需要数据绑定器里的转换服务(conversionService),而绑定服务里就有绑定器converters,负责数据类型转换

 小结:WebDataBinder利用它里面的Converters将请求数据转成指定的数据类型,再次封装到JavaBean中

绑定参数:

this.bindRequestParameters(binder, webRequest);

经过这一步,attribute和target里的属性都有了属性值

底层GenericConversionService:在设置每一个值的时候,找它里面所有的converters 具体哪个可以将这个数据类型(request带来的参数字符串)转换到指定的类型(JavaBean---Integer/String等)

未来我们可以给WebDataBinder里面放自己的Converter

private static final class StringToNumber<T extends Number> implements Converter<String,T>

7.11 自定义Converter原理

需求:将格式"阿猫,3"转换为宠物类型

    宠物:<input name="pet" value="阿猫,3">

配置类:addFormatters可以重写Converter和Formatter

@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new Converter<String,Pet>(){
            @Override
            public Pet convert(String source) {
                if(!StringUtils.isEmpty(source)){
                    Pet pet = new Pet();
                    String[] split = source.split(",");
                    pet.setName(split[0]);
                    pet.setAge(Integer.parseInt(split[1]));
                    return pet;
                }
                return null;
            }
        });
    }
}

运行结果:

八、响应处理

8.1 ReturnValueHandler原理

引入web场景:

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

(通过引入web场景)自动引入json场景:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-json</artifactId>
      <version>3.0.4</version>
      <scope>compile</scope>
    </dependency>

而json底层依赖jackson做数据转换与处理,使得json处理特别简单

    <dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jdk8</artifactId>
      <version>2.14.2</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jsr310</artifactId>
      <version>2.14.2</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.module</groupId>
      <artifactId>jackson-module-parameter-names</artifactId>
      <version>2.14.2</version>
      <scope>compile</scope>
    </dependency>

只要给方法上标注@ResponseBody,就可以自动给前端返回json数据

    @GetMapping("/test/person")
    @ResponseBody
    public Person getPerson(){
        Person person=new Person();
        person.setAge(28);
        person.setBirth(new Date());
        person.setUserName("zhangsan");
        return person;
    }

原理剖析:

返回值解析器ReturnValueHandler

 处理返回值:

            this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);

关键方法selectHandler:寻找返回值处理器来处理返回值

    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
        if (handler == null) {
            throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
        } else {
            handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
        }
    }

判断处理器是否是异步的:

 判断不是异步以后,来到真正的逻辑处理:

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

        HandlerMethodReturnValueHandler handler;
        do {
            do {
                if (!var4.hasNext()) {
                    return null;
                }

                handler = (HandlerMethodReturnValueHandler)var4.next();
            } while(isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler));
        } while(!handler.supportsReturnType(returnType));

最后,返回值处理器调用handleReturnValue进行处理

    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
        if (handler == null) {
            throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
        } else {
            handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
        }
    }

SpringMVC到底支持哪些返回值?

ModelAndView

Model

View

ResponseEntity

ResponseBodyEmitter

StreamingResponseBody

HttpEntity

HttpHeaders

Callable

DeferredResult

ListenableFuture

CompletionStage

WebAsyncTask

有@ModelAttribute注解

返回值标注了@ResponseBody===>用RequestResponseBodyMethodProcessor

    public boolean supportsReturnType(MethodParameter returnType) {
        return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class);
    }

总结 返回值解析器原理:

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

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

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

8.2 HTTPMessageConverter原理

真正处理返回值的方法:(方法含义:使用消息转换器来进行写出操作)

this.writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);

1.利用 MessageConverters 进行处理并将数据写为json格式

        i.内容协商(浏览器默认会以请求头的方式告诉服务器它能接收什么样的内容数据)

 q表示权重,q=0.9>q=0.8,所以浏览器优先接收页面

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

        iii.SpringMVC会挨个遍历所有容器底层的消息转换器MessageConverter,看谁能处理数据

                得到MappingJackson2HttpMessageConverter可以将对象写为json

①拿到浏览器可接收的类型:

 ②拿到服务器能响应的类型:

消息转换器可以处理的情况(规范):

        消息转换器的作用是看是否支持将此Class类型的对象转为MediaType类型的数据

        例子:Person对象转为JSON或JSON转为Person

默认的MessageConverter:

 0--只支持Byte类型

1--String

2--String

3--Resource

4--ResourceRegion

5--DOMSource.class\SAXSource.class\StAXSource.class\StreamSource.class\Source.class

6/7 --true只返回true,能处理所有类型

    protected boolean supports(Class<?> clazz) {
        return true;
    }

当消息转换器为MappingJackson2HttpMessageConverter的时候:

canWrite方法:浏览器适配、服务器支持

    public boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType) {
        return this.canWrite(clazz, mediaType);
    }
    protected boolean canWrite(@Nullable MediaType mediaType) {
        if (mediaType != null && !MediaType.ALL.equalsTypeAndSubtype(mediaType)) {
            Iterator var2 = this.getSupportedMediaTypes().iterator();

            MediaType supportedMediaType;
            do {
                if (!var2.hasNext()) {
                    return false;
                }

                supportedMediaType = (MediaType)var2.next();
            } while(!supportedMediaType.isCompatibleWith(mediaType));

            return true;
        } else {
            return true;
        }
    }

判断成功后拿到body,即需要响应的内容:

        可以看到body就是一个Person对象,想要以json的方式写出去

         写之前会添加一些头部信息:

this.addContentDispositionHeader(inputMessage, outputMessage);

        然后使用消息转换器的write方法将body写出:

genericConverter.write(body, (Type)targetType, selectedMediaType, outputMessage);
this.writeInternal(t, type, outputMessage);

最后通过writeValue写出去,刷新流

                objectWriter = this.customizeWriter(objectWriter, javaType, contentType);
                objectWriter.writeValue(generator, value);
                this.writeSuffix(generator, object);
                generator.flush();

writeValue的参数value仍然是一个Person对象

在响应体中,让数据流以字符串形式展现:

最终MappingJackson2HttpMessageConverter把对象转为json数据(利用底层的jackson包中的objectMapper转换的)

8.3 内容协商原理

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

以xml和json为例,先导入xml的依赖:

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

利用postman对接收数据格式进行限制和测试:

内容协商原理:

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

        如果有,则用已经确定的媒体类型

        MediaType contentType = outputMessage.getHeaders().getContentType();

2.获取客户端(postman、浏览器)支持的内容类型

        获取客户端Accept请求头字段

  acceptableTypes = this.getAcceptableMediaTypes(request);

 3.获取服务器能支持(产生)的数据类型

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

List<MediaType> producibleTypes = this.getProducibleMediaTypes(request, valueType, (Type)targetType);

先获取到所有的转换器:

 4.找到支持Person的converter,把converter支持的媒体类型统计出来

                while(var6.hasNext()) {
                    HttpMessageConverter<?> converter = (HttpMessageConverter)var6.next();
                    if (converter instanceof GenericHttpMessageConverter && targetType != null) {
                        if (((GenericHttpMessageConverter)converter).canWrite(targetType, valueClass, (MediaType)null)) {
                            result.addAll(converter.getSupportedMediaTypes(valueClass));
                        }
                    } else if (converter.canWrite(valueClass, (MediaType)null)) {
                        result.addAll(converter.getSupportedMediaTypes(valueClass));
                    }
                }

5.客户端需要application/xml。服务器端能处理的类型如下

 6.双重循环,进行内容协商的最佳匹配(拿到浏览器想要的类型和我支持的类型)

this.determineCompatibleMediaTypes(acceptableTypes, producibleTypes, compatibleMediaTypes);
    private void determineCompatibleMediaTypes(List<MediaType> acceptableTypes, List<MediaType> producibleTypes, List<MediaType> mediaTypesToUse) {
        Iterator var4 = acceptableTypes.iterator();

        while(var4.hasNext()) {
            MediaType requestedType = (MediaType)var4.next();
            Iterator var6 = producibleTypes.iterator();

            while(var6.hasNext()) {
                MediaType producibleType = (MediaType)var6.next();
                if (requestedType.isCompatibleWith(producibleType)) {
                    mediaTypesToUse.add(this.getMostSpecificMediaType(requestedType, producibleType));
                }
            }
        }

    }

最佳匹配结果:

 虽然匹配出来的结果很多,但selectMediaType一旦确定,就不会继续匹配了

            while(var15.hasNext()) {
                MediaType mediaType = (MediaType)var15.next();
                if (mediaType.isConcrete()) {
                    selectedMediaType = mediaType;
                    break;
                }
            //后略
            }

 7.用支持将对象转为最佳匹配媒体类型的converter,调用它进行转换

8.4 基于请求参数的内容协商原理

获取客户端(postman、浏览器)支持接收的内容类型(获取客户端Accept请求头字段)

关键方法:

    private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
        return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
    }

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

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

获取请求头的Accept内容:

String[] headerValueArray = request.getHeaderValues("Accept");

尽管postman测试可以很方便的改变请求头中的Accept的内容,但是浏览器本身发请求,请求头很难改变

SpringBoot支持开启浏览器参数方式内容协议功能:

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true

只要路由中加上format字段加以限定即可:

http://localhost:8080/test/person?format=json

或者http://localhost:8080/test/person?format=xml

打开favor-parameter,内容协商管理器会新增一个基于参数的策略,而这个参数就是format

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

        Parameter策略优先确定返回xml数据(获取请求头中的format值)

 8.5 自定义MessageConverter

实现多协议数据兼容:json/xml/x-guigu

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

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

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

3.内容协商找到最终的messageConverter

需求:

        1.浏览器发送请求直接返回xml

        2.如果是ajax请求,返回json 

        3.如果是硅谷app发送请求,返回自定义协议数据

步骤:

        1.添加自定义的MessageConverter进系统底层

        2.系统底层就会统计出所有MessageConverter能操作哪些类型

        3.客户端内容协商

导入了jackson处理xml的包,xml的converter就会自动进来

WebMvcAutoConfiguration类:

        configureMessageConverters方法:将所有获取到的converter全部加载进来

        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            this.messageConvertersProvider.ifAvailable((customConverters) -> {
                converters.addAll(customConverters.getConverters());
            });
        }

getConverters方法底层添加了默认的converters,还实现了按需加载:

默认添加:

    protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        messageConverters.add(new ByteArrayHttpMessageConverter());
        messageConverters.add(new StringHttpMessageConverter());
        messageConverters.add(new ResourceHttpMessageConverter());
        messageConverters.add(new ResourceRegionHttpMessageConverter());
        messageConverters.add(new AllEncompassingFormHttpMessageConverter());
//后略
}

按需加载:

导入jackson处理xml的包:

        if (jackson2XmlPresent) {
            builder = Jackson2ObjectMapperBuilder.xml();
            if (this.applicationContext != null) {
                builder.applicationContext(this.applicationContext);
            }

            messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
        } 

导入web场景:

        if (jackson2Present) {
            builder = Jackson2ObjectMapperBuilder.json();
            if (this.applicationContext != null) {
                builder.applicationContext(this.applicationContext);
            }

            messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
        }

按需加载原理:判断对应的类有没有导入(以jackson处理xml的包为例)


    static {
        ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
        romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
        jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader);
        jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
        jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
        jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
        jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
        gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
        jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
        kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader);
        kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
        kotlinSerializationProtobufPresent = ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader);
    }

自定义MessageConverters实现:

//自定义的Converter
public class GuiguMessageConverter implements HttpMessageConverter<Person> {


    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return false;
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return clazz.isAssignableFrom(Person.class);
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return MediaType.parseMediaTypes("application/x-guigu");
    }

    @Override
    public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    @Override
    public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        //自定义协议数据写出
        String data=person.getUserName()+";"+person.getAge()+";"+person.getBirth();
        //写出去
        OutputStream body = outputMessage.getBody();
        body.write(data.getBytes());
    }
}

将自定义的实现类加入converters里:


@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new GuiguMessageConverter());
    }
}

debug模式:

拿到内容协商协议

 服务器中所有的Converter:

 所有的converter合起来能生产的数据类型:

进行内容协商处理……

运行截图:

8.6 浏览器与postman内容协商完全适配

需求:

通过url:http://localhost:8080/test/person?format=gg 得到自定义格式的数据

代码实现:

@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        Map<String, MediaType> mediaTypes=new HashMap<>();
        //指定支持解析哪些参数对应的哪些媒体类型
        mediaTypes.put("json",MediaType.APPLICATION_JSON);
        mediaTypes.put("xml",MediaType.APPLICATION_XML);
        mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu"));
        ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
        configurer.strategies(Arrays.asList(parameterStrategy));
    }

debug模式:

适配器策略===>客户端能发起的数据类型:

 服务器端能产生的数据类型:

 运行截图:

在用postman测试的时候,虽然通过参数方式携带数据可以返回对应的格式,但使用accept请求头的方式,返回值都是json数据格式

 解决方法:添加请求头协商策略

        HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();
        configurer.strategies(Arrays.asList(parameterStrategy,headerStrategy));
    

完美解决:

小结:

        有可能我们添加的自定义功能会覆盖默认的很多功能,导致一些默认的功能失效,需要注意

修改请求参数的名字的方式:

①  parameterStrategy.setParameterName("ff");

②  mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu"));

 九、视图解析

9.1 Thymeleaf初体验

视图解析:SpringBoot默认不支持JSP,需要引入第三方模板引擎技术实现页面渲染

 第一步:引入Thymeleaf启动场景

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

 此时SpringBoot已经自动配置好了thymeleaf

@AutoConfiguration(
    after = {WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class}
)
@EnableConfigurationProperties({ThymeleafProperties.class})
@ConditionalOnClass({TemplateMode.class, SpringTemplateEngine.class})
@Import({ReactiveTemplateEngineConfiguration.class, DefaultTemplateEngineConfiguration.class})
public class ThymeleafAutoConfiguration {

官方自动配好的策略:

① 所有thymeleaf的配置值都在ThymeleafProperties

②配置好了SpringTemplateEngine

③配置好了ThymeleafViewResolver

===>我们只需要直接开发页面

ThymeleafProperties默认配置了前缀和后缀:

    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";

第二步:引入Thymeleaf名称空间

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

第三步:编写代码测试

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1 th:text="${msg}">哈哈</h1>
<h2>
    <a href="www.atguigu.com" th:href="${link}">去百度</a>
    <a href="www.atguigu.com" th:href="@{/link}">去百度</a>
</h2>
</body>
</html>

ViewTestController

@Controller
public class ViewTestController {

    @GetMapping("/atguigu")
    public String atguigu(Model model){
        //model中的数据会被放在请求域中 相当于 request.setAttribute()
        model.addAttribute("msg","你好,尚硅谷");
        model.addAttribute("link","http://www.baidu.com");
        return "success";
    }
}

查看网页源代码可以发现@{}解析的路径会把写入的内容直接当路径

 9.2 web实验

9.2.1 解决表单重复提交

因为访问主页面的路径还是/login,一刷新就会导致表单的重复提交。为了解决这个问题,可以使用重定向

@Controller
public class indexController {

    @GetMapping(value={"/","/login"})
    public String loginPage(){
        return "login";
    }

    @PostMapping("/login")
    public String main(String username,String password){
        //登录成功重定向到main.html
        return "redirect:/main.html";
    }

    @GetMapping("/main.html")
    public String mainPage(){

        return "main";
    }
}

9.2.2 完善登录的逻辑判断

    @PostMapping("/login")
    public String main(User user, HttpSession session, Model model){
        if(!StringUtils.isEmpty(user.getUsername()) && "123456".equals(user.getPassword())){
            session.setAttribute("loginUser",user);
            //登录成功重定向到main.html
            return "redirect:/main.html";
        }else {
            model.addAttribute("msg","账号密码错误");
            return "login";
        }
    }

    @GetMapping("/main.html")
    public String mainPage(HttpSession session,Model model){
        Object loginUser = session.getAttribute("loginUser");
        if(loginUser!=null){
            return "main";
        }else{
            model.addAttribute("msg","未登录,请重新登录");
            return "login";
        }
    }

thymeleaf的行内样式写法:

        [[${session.user.name}]]

9.2.3 抽取公共页面

前置步骤:

编写接口:

@Controller
public class TableController {

    @GetMapping("/basic_table")
    public String basic_table(){
        return "table/basic_table";
    }

    @GetMapping("/dynamic_table")
    public String dynamic_table(){
        return "table/dynamic_table";
    }

    @GetMapping("/responsive_table")
    public String responsive_table(){
        return "table/responsive_table";
    }

    @GetMapping("/editable_table")
    public String editable_table(){
        return "table/editable_table";
    }

}

如果想要thymeleaf解析页面,需要为每个页面加上xmlns:th="http://www.thymeleaf.org"名称空间

并且修改超链接跳转:

                <li class="menu-list"><a href="#"><i class="fa fa-th-list"></i> <span>Data Tables</span></a>
                    <ul class="sub-menu-list">
                        <li><a href="basic_table.html" th:href="@{/basic_table}"> Basic Table</a></li>
                        <li><a href="dynamic_table.html" th:href="@{/dynamic_table}"> Advanced Table</a></li>
                        <li><a href="responsive_table.html" th:href="@{/responsive_table}"> Responsive Table</a></li>
                        <li><a href="editable_table.html" th:href="@{/editable_table}"> Edit Table</a></li>
                    </ul>
                </li>

头部抽取:

<head th:fragment="commonheader">
    //css样式等引入,略
</head>
    <div th:include="common :: commonheader"></div>

尾部js引入:

<div id="commonscript"></div>
<div th:replace="common :: #commonscript"></div>

左侧菜单引入:

<div id="leftmenu" class="left-side sticky-left-side"></div>
    <div th:replace="common :: #leftmenu"></div>

头部菜单引入:

<div th:fragment="headermenu" class="header-section"></div>
        <div th:replace="common :: headermenu"></div>

9.2.4 遍历数据

接口:

    @GetMapping("/dynamic_table")
    public String dynamic_table(Model model){
        //表格内容的遍历
        List<User> users = Arrays.asList(new User("zhangsan", "234321"),
                new User("lisi", "453241"),
                new User("wangwu", "652524"));
        model.addAttribute("users",users);
        return "table/dynamic_table";
    }

页面动态数据加载:


        <table  class="display table table-bordered table-striped" id="dynamic-table">
        <thead>
        <tr>
            <th>#</th>
            <th>用户名</th>
            <th>密码</th>
        </tr>
        </thead>
        <tbody>
        <tr class="gradeX" th:each="user,status:${users}">
            <td th:text="${status.count}"></td>
            <td th:text="${user.username}"></td>
            <td>[[${user.password}]]</td>
        </tr>
        </tbody>
        </table>

效果图:

9.3 视图解析器与视图解析源码分析

第一步:寻找当前这个请求由哪个handler处理===>indexController

mappedHandler = this.getHandler(processedRequest);

第二步:寻找处理器的适配器===>RequestMappingHandlerAdapter

HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());

第三步:适配器调用方法

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

第四步:执行目标方法

mav = this.invokeHandlerMethod(request, response, handlerMethod);

第五步:确定参数和返回值的处理器

        if (this.argumentResolvers != null) {
            invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
        }

        if (this.returnValueHandlers != null) {
            invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
        }

第六步:实现重定向后,对返回值进行解析:找到返回值处理器

            this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);

 第七步:选择合适的返回值处理器

HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);

ViewNameMethodReturnValueHandler可以处理返回值为空或为字符序列方法

        ===>返回值处理器为ViewNameMethodReturnValueHandler

    public boolean supportsReturnType(MethodParameter returnType) {
        Class<?> paramType = returnType.getParameterType();
        return Void.TYPE == paramType || CharSequence.class.isAssignableFrom(paramType);
    }

第八步:处理器开始处理返回值

handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);

①如果返回值是字符序列,就将它转换为字符串

②将得到的字符串放入mavContainer中

③判断返回值是否需要重定向

protected boolean isRedirectViewName(String viewName) {
    return PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:");
}

④如果需要重定向,则打开重定向传感器

    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        if (returnValue instanceof CharSequence) {
            String viewName = returnValue.toString();
            mavContainer.setViewName(viewName);
            if (this.isRedirectViewName(viewName)) {
                mavContainer.setRedirectModelScenario(true);
            }
        } else if (returnValue != null) {
            String var10002 = returnType.getParameterType().getName();
            throw new UnsupportedOperationException("Unexpected return type: " + var10002 + " in method: " + returnType.getMethod());
        }

    }

扩展:如果方法没有返回名字,会调用applyDefaultViewName

this.applyDefaultViewName(processedRequest, mv);

如果视图为空,会返回默认的跳转页

    private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
        if (mv != null && !mv.hasView()) {
            String defaultViewName = this.getDefaultViewName(request);
            if (defaultViewName != null) {
                mv.setViewName(defaultViewName);
            }
        }

    }

第九步:处理派发结果

this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);

===>视图解析原理流程:

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

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

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

4.processDispatchResult 处理派发结果(页面该如何响应)

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

                i.根据方法的String返回值得到View对象

view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);

View实际上是一个接口,定义了render函数(即页面渲染的逻辑)

    void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

调用resolveViewName方法得到视图解析器===>所有的视图解析器尝试是否能根据当前返回值得到View对象

         2.通过ContentNegotiatingViewResolver得到了最佳匹配视图:RedirectView

        redirect:/main.html ==>Thymeleaf视图解析器===> new了一个RedirectView并返回

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

         4.视图对象调用render方法进行渲染

        view.render(mv.getModelInternal(), request, response);

 ①合并Model对象设置的内容

Map<String, Object> mergedModel = this.createMergedOutputModel(model, request, response);

②重定向页面

this.renderMergedOutputModel(mergedModel, this.getRequestToExpose(request), response);

RedirectView 如何渲染:

a.获取目标url地址

b.response.sendRedirect(encodedURL);重定向到指定的地址

    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws IOException {
        String targetUrl = this.createTargetUrl(model, request);
        targetUrl = this.updateTargetUrl(targetUrl, model, request, response);
        RequestContextUtils.saveOutputFlashMap(targetUrl, request, response);
        this.sendRedirect(request, response, targetUrl, this.http10Compatible);
    }

视图解析:

返回值以forward:开始==>new InternalResourceView(forwardUrl);==>转发

        request.getRequestDispatcher(path).forward(request,response);

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

十、拦截器

10.1 登录检查与静态资源放行

拦截器接口:HandlerInterceptor

preHandle:目标方法执行之前

postHandle:目标方法执行完成以后

afterCompletion:页面渲染完成之后

public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

拦截器使用步骤:

①编写一个拦截器,实现HandlerInterceptor接口

②拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)

③指定拦截规则(如果是拦截所有,静态资源也会被拦截)

第一步:

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //登录检查逻辑
        HttpSession session = request.getSession();
        Object loginUser = session.getAttribute("loginUser");
        if(loginUser!=null){
            //放行
            return true;
        }
        //拦截住==>未登录,跳转至登录页
        session.setAttribute("msg","请先登录");
        request.getRequestDispatcher("/").forward(request,response);
        return false;
    }
    
}

第二步:

@Configuration
public class AdminWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") //拦截所有资源,包括静态资源
                .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行登录界面
    }
}

10.2 拦截器的执行时机与原理

第一步:找到合适的处理器

mappedHandler = this.getHandler(processedRequest);

第二步:在执行拦截器方法前先执行preHandle方法

                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                    }

第三步:对于所有的拦截器方法,先顺序执行preHandle方法

    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
            HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
            if (!interceptor.preHandle(request, response, this.handler)) {
                this.triggerAfterCompletion(request, response, (Exception)null);
                return false;
            }
        }

        return true;
    }

如果preHandle方法执行失败,则倒序执行每个拦截器的afterCompletion方法

    void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
        for(int i = this.interceptorIndex; i >= 0; --i) {
            HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);

            try {
                interceptor.afterCompletion(request, response, this.handler, ex);
            } catch (Throwable var7) {
                logger.error("HandlerInterceptor.afterCompletion threw exception", var7);
            }
        }

    }

第四步:执行目标方法

第五步:目标方法执行完毕后,执行postHandle方法

 mappedHandler.applyPostHandle(processedRequest, response, mv);

倒序执行所有拦截器的postHandle方法

    void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
        for(int i = this.interceptorList.size() - 1; i >= 0; --i) {
            HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
            interceptor.postHandle(request, response, this.handler, mv);
        }

    }

第六步:开始页面渲染

 this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);

拦截器原理:

       1. 根据当前请求,找到可以处理请求的handler以及handler的所有拦截器(即HandlerExecutionChain)

        2. 先来顺序执行所有拦截器的preHandle方法

                ①如果当前拦截器preHandle返回为true,则执行下一个拦截器的preHandle

                ②如果当前拦截器返回为false,直接倒序执行所有已经执行了的afterCompletion

        3.如果任何一个拦截器执行失败(返回false),则直接跳出不执行目标方法(ha.handle())

        4.所有拦截器都返回为true,则执行目标方法

        5.倒序执行所有拦截器的postHandle方法

        6.前面的步骤有任何异常都会直接触发afterCompletion方法

        7.页面成功渲染完成以后,也会倒序触发afterCompletion方法

十一、文件上传

 11.1 单文件与多文件上传的使用

文件上传时,表单必须做到:

添加enctype="multipart/form-data"

提交方式method="post"

文件上传输入框type="file"

多文件上传需要使用关键字mutipart

                        <form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
                            <div class="form-group">
                                <label for="exampleInputEmail1">邮箱</label>
                                <input type="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
                            </div>
                            <div class="form-group">
                                <label for="exampleInputPassword1">名字</label>
                                <input type="text" class="form-control" id="exampleInputPassword1" placeholder="请输入名字..." name="username">
                            </div>
                            <div class="form-group">
                                <label for="exampleInputFile">头像</label>
                                <input type="file" id="exampleInputFile" name="headerImg">
                            </div>
                            <div class="form-group">
                                <label for="exampleInputFile">生活照</label>
                                <input type="file"  name="photos" multiple>
                            </div>
                            <div class="checkbox">
                                <label>
                                    <input type="checkbox"> Check me out
                                </label>
                            </div>
                            <button type="submit" class="btn btn-primary">Submit</button>
                        </form>

接口:

使用@RequestPart来接收传过来的文件

使用MultipartFile作为单文件类型,使用MultipartFile[]作为多文件类型

先验证是否接收到上传的文件:

@Controller
@Slf4j
public class FormTestController {

    @GetMapping("/form_layouts")
    public String form_layouts(){
        return "form_layouts";
    }

    @PostMapping("/upload")
    public String upload(@RequestParam("email")String email,
                         @RequestParam("username")String username,
                         @RequestPart("headerImg")MultipartFile headerImg,
                         @RequestPart("photos")MultipartFile[] photos){

        log.info("上传的信息:email={},username={},headerImg={},photos={}",email,username,headerImg.getSize(),photos.length);
        return "main";
    }

}

将上传的文件保存到文件服务器:

    @PostMapping("/upload")
    public String upload(@RequestParam("email")String email,
                         @RequestParam("username")String username,
                         @RequestPart("headerImg")MultipartFile headerImg,
                         @RequestPart("photos")MultipartFile[] photos) throws IOException {

        log.info("上传的信息:email={},username={},headerImg={},photos={}",email,username,headerImg.getSize(),photos.length);

        if(!headerImg.isEmpty()){
            String originalFilename = headerImg.getOriginalFilename();
            headerImg.transferTo(new File("D:\\itcast\\"+originalFilename));
        }

        if(photos.length>0){
            for(MultipartFile photo:photos){
                String originalFilename = photo.getOriginalFilename();
                photo.transferTo(new File("D:\\itcast\\"+originalFilename));

            }
        }


        return "main";
    }

自定义修改可上传的文件大小:

spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB

11.2 文件上传参数解析器

文件上传自动配置类 MultipartAutoConfiguration

        自动配置好了 StandardServletMultipartResolver【文件上传解析器】

第一步:判断当前请求是否为文件上传请求

processedRequest = this.checkMultipart(request);

①使用文件上传解析器 判断并封装、返回文件上传请求

        if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {

判断:isMultipart方法

    public boolean isMultipart(HttpServletRequest request) {
        return StringUtils.startsWithIgnoreCase(request.getContentType(), this.strictServletCompliance ? "multipart/form-data" : "multipart/");
    }

解析:

return this.multipartResolver.resolveMultipart(request);
    public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
        return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
    }

第二步:适配器调用handle方法

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

②参数解析器来解析请求中的文件内容,封装成MultipartFile

③将request中文件信息封装成一个Map

十二、异常处理

12.1 SpringBoot默认错误处理机制

默认规则:

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

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

机器客户端(用Postman模拟):

 浏览器客户端:

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

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

error下的4xx/5xx页面会被自动解析

给模板引擎添加两个页面,会响应自定义内容

 

12.2 底层组件功能分析

进行异常处理的自动配置:ErrorMvcAutoConfiguration

        容器中的组件:

        ①类型:DefaultErrorAttributes-->id:errorAttributes

        public class DefaultErrorAttributes implements ErrorAttributes,         HandlerExceptionResolver, Ordered {

        ②类型:BasicErrorController-->id:basicErrorController

        处理默认 /error路径的请求

@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {

==>BasicErrorController中定义了要么响应页面,要么响应JSON数据

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

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = this.getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity(status);
        } else {
            Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
            return new ResponseEntity(body, status);
        }
    }

        组件:View-->id:error

        @Bean(
            name = {"error"}
        )
        @ConditionalOnMissingBean(
            name = {"error"}
        )
        public View defaultErrorView() {
            return this.defaultErrorView;
        }

        组件:BeanNameViewResolver(视图解析器),按照返回的视图名作为组件的id去容器中找View对象

        @Bean
        @ConditionalOnMissingBean
        public BeanNameViewResolver beanNameViewResolver() {
            BeanNameViewResolver resolver = new BeanNameViewResolver();
            resolver.setOrder(2147483637);
            return resolver;
        }

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

容器中的组件:类型:DefaultErrorViewResolver-->id:conventionErrorViewResolver

自动映射:

    static {
        Map<Series, String> views = new EnumMap(Series.class);
        views.put(Series.CLIENT_ERROR, "4xx");
        views.put(Series.SERVER_ERROR, "5xx");
        SERIES_VIEWS = Collections.unmodifiableMap(views);
    }

解析的时候会自动加上error/前缀

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

        error.viewName.html

    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        String errorViewName = "error/" + viewName;
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
        return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
    }

容器中的组件:DefaultErrorAttributes

        定义错误页面中可以包含哪些数据

    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
        if (!options.isIncluded(Include.EXCEPTION)) {
            errorAttributes.remove("exception");
        }

        if (!options.isIncluded(Include.STACK_TRACE)) {
            errorAttributes.remove("trace");
        }

        if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
            errorAttributes.remove("message");
        }

        if (!options.isIncluded(Include.BINDING_ERRORS)) {
            errorAttributes.remove("errors");
        }

        return errorAttributes;
    }

12.3 异常处理流程

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

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

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

this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);

3.处理handler发生的异常,处理完成返回ModelAndView

mv = this.processHandlerException(request, response, handler, exception);

        遍历所有的HandlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】

系统默认的异常解析器:

 HandlerExceptionResolver接口的方法resolveException:

        拿到原生的request,response,并返回一个视图ModelAndView

public interface HandlerExceptionResolver {
    @Nullable
    ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}

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

    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        this.storeErrorAttributes(request, ex);
        return null;
    }

    private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
        request.setAttribute(ERROR_INTERNAL_ATTRIBUTE, ex);
    }

②默认处理器中都不能处理异常,故异常会被抛出去

③如果没有任何处理器能处理,底层就会发送/error请求,并被底层的BasicErrorController处理

        i.发送/error请求

        ii.解析错误视图:遍历所有的ErrorViewResolver,看谁能解析

        iii.默认的DefaultViewResolver,作用是把响应状态码作为错误页的地址拼接成error/500.html等

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

12.4 几种异常处理原理

12.4.1 定制错误处理逻辑

自定义错误页

        error/404.html、error/5xx.html 有精确的错误状态码页面就精确匹配,没有就找4xx.html、5xx.html;如果都没有则触发白页

@ControllerAdvice+@ExceptionHandler处理全局异常

@ResponseStatus+自定义异常

Spring底层的异常,如参数类型转换异常

自定义实现HandlerExceptionResolver处理异常

12.4.2 @ControllerAdvice+@ExceptionHandler处理全局异常

底层ExceptionHandlerExceptionResolver提供处理zhichi

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler({ArithmeticException.class,NullPointerException.class})
    public String handleArithException(Exception e){
        log.error("异常是:{}",e);

        return "login";
    }

}

12.4.3 @ResponseStatus+自定义异常

底层是 ResponseStatusExceptionResolver支持

把responsestatus注解的信息组装ModelAndView返回

底层调用response.sendError(statusCode,resolvedReason);即tomcat发送/error

@ResponseStatus(value= HttpStatus.FORBIDDEN,reason="用户数量太多")
public class UserTooManyException extends RuntimeException{

    public UserTooManyException(){}

    public UserTooManyException(String message){
        super(message);
    }


}

自定义异常解析器:

@Order(value=HIGHEST_PRECEDENCE)
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

        try {
            response.sendError(511,"我喜欢的错误");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ModelAndView();

    }
}

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

13.1 原生注解与Spring方法注入

使用原生注解:

①主程序类使用@ServletComponentScan

@ServletComponentScan(basePackages = "com.example.webdemo.servlet")

②编写自己的Servlet        注意使用@WebServlet注解

Servlet:

@WebServlet(urlPatterns="/my")
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("666");
    }
}

效果:直接响应,没有Spring的拦截器

Filter:

@Slf4j
@WebFilter(urlPatterns = {"/css/*","/images/*"})
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("MyFilter初始化完成");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("MyFilter工作");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        log.info("MyFilter销毁");
    }
}

Listener:

@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        log.info("MyServletContextListener监听到项目初始化完成");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        log.info("MyServletContextListener监听到项目初始化销毁");
    }
}

Spring方法注入:

使用返回值ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean


@Configuration
public class MyRegistryConfig {

    public ServletRegistrationBean myServlet(){
        MyServlet myServlet = new MyServlet();
        return new ServletRegistrationBean(myServlet,"/my","/my02");
    }

    public FilterRegistrationBean myFilter(){
        MyFilter myFilter = new MyFilter();
        //拦截和myServlet定义的路径一样的路径
        return new FilterRegistrationBean(myFilter,myServlet());
    }

    public FilterRegistrationBean myFilterTest(){
        MyFilter myFilter = new MyFilter();
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
        return filterRegistrationBean;
    }

    public ServletListenerRegistrationBean myListener(){
        MyServletContextListener myServletContextListener = new MyServletContextListener();
        return new ServletListenerRegistrationBean(myServletContextListener);
    }


}

13.2 DispatchServlet注入原理

容器中自动配置了DispatcherServlet,属性绑定到WebMvcProperties,对应的配置项是spring.mvc

通过ServletRegistrationBean<DispatcherServlet> 把DispatcherServlet配置进来

默认servlet路径映射的是/路径

Tomcat处理Servlet原理:

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

13.3 嵌入式Servlet容器---切换web服务器与定制化

默认支持的webServer

        Tomcat、Jetty、Undertow

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

原理:

        SpringBoot应用启动发现当前是Web应用==>web场景导入tomact

        web应用会创建一个web版的ioc容器 ==>ServletWebServerApplicationContext

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

        SpringBoot底层默认有很多WebServer工厂:

                TomcatServletWebServerFactory、JettyServletWebServerFactory、UndertowServletWebServerFactory

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

        ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)

        ServletWebServerFactoryConfiguration配置类根据动态判断系统中到底导入了哪个web服务器包(默认web-starter导入tomcat包),所以容器中就有一个tomcat的wei服务器工厂

        TomcatServletWebServerFactory创建出Tomcat服务器并启动,TomcatWebServer的构造器拥有初始化方法

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

切换服务器:

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

13.4 SpringBoot定制化组件的几种方式

原理分析套路:

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

定制化的常见方式:

①修改配置文件

②编写自定义的配置类 xxxConfiguation + @Bean替换、增加容器中默认组件;视图解析器

③web应用 实现WebMvvcConfigurer 即可定制化web功能

④@EnableWebMvc + WebMvcConfigurer——@Bean 可以全面接管SpringMVC,所有规则全部自己重新配置,实现定制和扩展功能

十四、数据访问

14.1 数据库场景的自动配置分析与整合测试

1.导入JDBC场景

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

2.分析自动配置

HikariCP:优秀的数据源连接池

spring-tx:事务管理

 分析:为什么导入JDBC场景,官方不导入驱动?

理由:官方不知道我们接下来要操作什么数据库

导入mysql数据库:

想要修改版本:

        1.直接依赖引入具体的版本(maven的就近依赖原则)

        2.重新声明仲裁版本(maven属性的就近原则)

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>

自动配置的类:

DataSourceAutoConfiguration:数据源的自动配置

        修改数据源相关的配置:spring.datasource

        数据库连接池的配置:是自己容器中没有DataSource才自动配置的

        底层配置好的数据源是Hikari

DataSourceTransactionManagerAutoConfiguration:事务管理的配置

JdbcTemplateAutoConfiguration:JdbcTemplate的自动配置,可以对数据库进行crud

        可以修改这个配置项@ConfigurationProperties(prefix="spring.jdbc")来修改jdbcTemplat

JndiDataSourceAutoConfiguration:jndi的自动配置

XADataSourceAutoConfiguration:分布式事务的自动动配置

导入数据源配置:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/company
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

测试:

@SpringBootTest
class WebDemoApplicationTests {


    @Autowired
    JdbcTemplate jdbcTemplate;


    @Test
    void contextLoads() {
        List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from class");
        maps.forEach(System.out::println);
    }

}

14.2 自定义方式整合Druid数据源

整合第三方技术的两种方式:

①自定义

②找starter

第一步:引入依赖

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.12</version>
        </dependency>

第二步:配置

@Configuration
public class MyDataSourceConfig {

    @ConfigurationProperties("spring.datasource")
    @Bean
    public DataSource dataSource(){
        return new DruidDataSource();
    }


}

此时,springboot底层的数据源就变成了Druid

怎么使用Druid内置监控页面?

    /*配置druid的监控页功能*/
    @Bean
    public ServletRegistrationBean statViewServlet(){
        return new ServletRegistrationBean<StatViewServlet>(new StatViewServlet(),"/druid/*");
    }

开启SQL监控:

        druidDataSource.setFilters("stat");

配置内置监控中的Web和Spring的关联监控:

    @Bean
    public FilterRegistrationBean webStatFilter(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter((Filter) new WebStatFilter());
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
        filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return filterRegistrationBean;
    }
        druidDataSource.setFilters("stat,wall");

14.3 druid数据源starter整合方式

引入官方的starter:

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.9</version>
        </dependency>

自动配置:

扩展配置项 spring.datasource.druid

DruidSpringAopConfiguration.class:

        监控SpringBean

        配置项:spring.datasource.druid.aop-patterns

DruidStatViewServletConfiguration.class

        监控页的配置

        配置项:spring.datasource.druid.stat-view-servlet        默认开启

DruidWebStatFilterConfiguration.class

        web监控配置

        配置项:spring.datasource.druid.web-stat-filter        默认开启        

DruidFilterConfiguration.class

        所有Druid自己的filter的配置

配置application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db1
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      stat-view-servlet:
        enabled: true
        login-username: admin
        login-password: 123456
        resetEnable: false
      web-stat-filter:
        enabled: true
        urlPattern: /*
        exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
      aop-patterns: com.example.webdemo.*
      filters: stat,wall
      filter:
        stat:
          slow-sql-millis: 1000
          log-slow-sql: true
          enabled: true
        wall:
          enabled: true
          config:
            update-allow: false

14.4 整合mybatis---配置版

引入依赖:

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

配置模式:

全局配置文件

SqlSessionFactory:自动配置好了

SqlSession:自动配置了SqlSessionTemplate组合了SqlSession

Mapper:只要我们写的操作MyBatis的接口标注了@Mapper注解 就会被自动扫描进来

MybatisAutoConfiguration:

@EnableConfigurationProperties({MybatisProperties.class})==>MyBatis配置项绑定类

可以修改配置文件中mybatis开始的所有配置,来修改MyBaits配置

@ConfigurationProperties(
    prefix = "mybatis"
)
public class MybatisProperties {

配置application.yaml

#配置mybatis的规则
mybatis:
  config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mapper/*.xml

配置mybatis核心文件:

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--
        mybatis核心配置文件中的标签必须按照指定的顺序进行配置:
        properties typeAliases environments mappers
    -->
    <typeAliases>
        <package name="com.atguigu.test.pojo"></package>
    </typeAliases>
</configuration>

编写代码:

实体类:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dept {
    private int deptno;
    private String dname;
    private String loc;
}

mapper接口:

@Mapper
public interface DeptMapper {

    Dept getDept(int deptno);

}

xml对应接口:

<mapper namespace="com.atguigu.test.mapper.DeptMapper">
    <!--
        mapper接口和映射文件要保证两个一致:
        1.mapper接口的全类名和映射文件的namespace一致
        2.mapper接口中的方法的方法名要和映射文件中的sql的id保持一致
    -->
    <select id="getDept" parameterType="int" resultType="dept">
        select * from dept where deptno=#{deptno}
    </select>

</mapper>

测试:

@Controller
public class IndexController {

    @Autowired
    DeptMapper deptMapper;

    @GetMapping("/dept")
    @ResponseBody
    public Dept getByDeptno(){
        Dept dept = deptMapper.getDept(10);
        return dept;
    }

}

 注意:可以不写全局配置文件,而所有的全局配置文件的配置都放在configuration下

#配置mybatis的规则
mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
  type-aliases-package: com.atguigu.test.pojo

十五、单元测试

15.1 JUnit5 简介

SpringBoot 2.2.0 版本开始引入JUnit5 作为单元测试的默认库

JUnit5 = Junit Platform + JUnit Jupiter + Junit Vintage

 

SpringBoot整合JUnit以后:

编写测试方法:@Test注解标注即可(对应junit5版本的注解org.junit.jupiter下的注解)

JUnit类具有Spring的功能,可以使用@Autowired、@Transactional等

15.2 JUnit5 常用注解

@Test:表示方法是测试方法,但是与JUnit4的@Test不同,它的职责非常单一,不能声明任何属性,扩展的测试将会由Jupiter提供额外测试

@ParameterizedTest:表示方法是参数化测试

@RepeatedTest:表示方法可重复执行

@DisplayName:为测试类或者测试方法设置展示名称

@BeforeEach:表示在每个单元测试之前执行

@AfterEach:表示在每个单元测试之后执行

@BeforeAll:表示在所有单元测试之前执行

@AfterAll:表示在所有单元测试之后执行

@Tag:表示单元测试类别,类似于JUnit4中的@Categories

@Disabled:表示测试类或测试方法不执行,类似于JUnit4de@Ignore

@Timeout:表示测试方法运行如果超过了指定时间将会返回错误

@ExtendWith:为测试类或方法提供扩展类引用

@SpringBootTest是一个复合注解,内含@ExtendWith

@SpringBootTest
class TestApplicationTests {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Test
    @DisplayName("测试displayname注解")
    void testDisplayName(){
        System.out.println(1);
        System.out.println(jdbcTemplate);
    }

    @Test
    @Disabled
    @DisplayName("测试方法2")
    void test2(){
        System.out.println(2);
    }

    @Test
    @Timeout(value=500,unit= TimeUnit.MILLISECONDS)
    void testTimeout() throws InterruptedException {
        Thread.sleep(600);
    }

    @Test
    @RepeatedTest(5)
    void testRepeated(){
        System.out.println("repeated");
    }

    @BeforeEach
    void testBeforeEach(){
        System.out.println("测试就要开始了...");
    }

    @AfterEach
    void testAfterEach(){
        System.out.println("测试结束了...");
    }

    @BeforeAll
    static void testBeforeAll(){
        System.out.println("所有测试就要开始了...");
    }

    @AfterAll
    static void testAfterAll(){
        System.out.println("所有测试已经结束了...");
    }
}

15.3 断言机制

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。

可以检察业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告

Junit5内置的断言可以分为以下几类:

 

2.数组断言

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

    @Test
    void testArray(){
        Assertions.assertArrayEquals(new int[]{1,2},new int[]{1,2});
    }

测试通过~

3.组合断言

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

assertAll 全部的断言成功,才算成功

    @Test
    void testAll(){
        assertAll("test",()->assertTrue(true&&true),
                ()->assertEquals(1,1));
    }

4.异常断言

Assertions.assertThrows(),配合函数式编程使用

断定业务逻辑一定出现异常

    @Test
    void testException(){
        assertThrows(ArithmeticException.class,()->{int i=10/0;},"业务逻辑居然正常运行了");
    }

5.快速失败

希望业务逻辑快速失败

    @Test
    void testFail(){
        if(2==2){
            fail("测试失败");
        }
    }

15.4 前置条件(assumptions)

JUnit 5中的前置条件(assumptions 假设)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行中止。

前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要

    @Test
    void testAssumptions(){
        Assumptions.assumeTrue(true,"结果不是true");
        System.out.println(111);
    }

15.5 嵌套测试

JUnit 5可以通过Java中的内部类和@Nested注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。

在内部类中可以使用@BeforeEach和@AfterEach注解,而且嵌套的层次没有限制

在嵌套测试的情况下,外层的Test不能驱动内层的Before(After)Each/All之类的方法提前/延后执行

内层的Test可以驱动外层的Before(After)Each/All


@SpringBootTest
public class TestStackDemo {
    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());
            }
        }

    }



}

15.6 参数化测试

@SpringBootTest
public class TestAStackDemo {

    Stack<Object> stack;

    @ParameterizedTest
    @DisplayName("参数化测试")
    @ValueSource(ints = {1,2,3,4,5})
    void testParameterized(int i){
        System.out.println(i);
    }

    @ParameterizedTest
    @DisplayName("方法式测试")
    @MethodSource("stringProvider")
    void testParameterized2(String str){
        System.out.println(str);
    }

    static Stream<String> stringProvider(){
        return Stream.of("apple","banana","orange");
    }


}

 

十六、指标监控

16.1 SpringBoot Actuator 与 EndPoint

SpringBoot Actuator:

简介

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

1.x与2.x的区别

 

使用方法

第一步:引入场景

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

第二步:访问http://localhost:8080/actuator/**

暴露Endpoints

支持的暴露方式:

HTTP:默认值暴露health和info Endpoint

JMX:默认暴露所有的Endpoint

除了health和info,剩下的Endpoint都应该进行保护访问,如果引入SpringSecurity,则会默认配置安全访问规则

management是所有actuator的配置

management:
  endpoints:
    enabled-by-default: true #默认开启所有监控端点
    web:
      exposure:
        include: '*' #以web方式暴露所有端点

16.2 禁用与开启

最常用的Endpoint:

        Health:监控状况

        Metrics:运行时指标

        Loggers:日志记录

Health Endpoint

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

重要的几点:

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

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

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

management是所有actuator的配置

management.endpoint.端点名.xxx 对某个端点的具体配置

management:
  endpoints:
    enabled-by-default: true #默认开启所有监控端点
    web:
      exposure:
        include: '*' #以web方式暴露所有端点
  endpoint:
    health:
      show-details: always

 

Metrics Endpoint

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

通过Metrics对接多种监控系统

简化核心Metrics开发

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

开启与禁用endpoints:

默认所有的Endpoint 除了shutdown以外的都是开启的

需要开启或者禁用某个Endpoint。配置模式为:

management.endpoint.<endpointName>.enabled=true

management:
  endpoints:
    enabled-by-default: false #默认开启所有监控端点
    web:
      exposure:
        include: '*' #以web方式暴露所有端点
  endpoint:
    health:
      show-details: always
      enabled: true
    info:
      enabled: true
    beans:
      enabled: true

16.3 定制Endpoint

定制health:

@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {
    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        Map<String,Object> map=new HashMap<>();
        if(1==1){
//            builder.up();//健康
            builder.status(Status.UP);
            map.put("count",1);
            map.put("ms",100);
        }else{
            builder.status(Status.OUT_OF_SERVICE);
            map.put("err","连接超时");
            map.put("ms",3000);
        }
        builder.withDetail("code",100)
                .withDetails(map);
    }
}

定制info:

注意要开启management.info.env.enabled=true,否则收到的info页面为空{}

management:
  info:
    env:
      enabled: true
  endpoints:
    enabled-by-default: false #默认开启所有监控端点
    web:
      exposure:
        include: '*' #以web方式暴露所有端点
  endpoint:
    health:
      show-details: always
      enabled: true
    beans:
      enabled: true
    info:
      enabled: true
info:
  appName: boot-admin
  appVersion: 1.0.2

 

也可以通过自定义组件定制info:

@Component
public class ExampleInfoContributor implements InfoContributor {
    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("msg","你好")
                .withDetail("hello","atguigu")
                .withDetails(Collections.singletonMap("world","666"));
    }
}

定制Metrics:

构造器注入

@Service
public class DeptService {
    @Autowired
    DeptMapper deptMapper;

    Counter counter;
    public DeptService(MeterRegistry meterRegistry){
        counter = meterRegistry.counter("deptService.getDept.count");

    }

    public Dept getDept(int deptno){
        counter.increment();
        return deptMapper.getDept(deptno);
    }
}

也可以直接给容器中放一个MeterBinder

自定义端点不需要手动开启

@Component
@Endpoint(id="myservice")
public class MyServiceEndPoint {

    @ReadOperation
    public Map getDockerInfo(){
        //端点的读操作
        return Collections.singletonMap("docker","docker started...");
    }

    @WriteOperation
    public void stopDocker(){
        System.out.println("docker stopped...");
    }
}

16.4 Boot Admin Server

第一步:导入依赖

        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-server</artifactId>
            <version>2.3.1</version>
        </dependency>

第二步:在启动类上加注解@EnableAdminServer

@EnableAdminServer
@SpringBootApplication
public class TestApplication {

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

}

第三步:配置收集数据的url

spring:
  boot:
    admin:
      client:
        url: http://localhost:8080


后会有期

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值