IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> SpringBoot自学笔记 -> 正文阅读

[Java知识库]SpringBoot自学笔记

显示在配置和自动配置类上评估的条件,以及它们匹配或不匹配的原因。 基础入门

环境:

java 8+ :https://www.oracle.com/cn/java/technologies/javase/javase-jdk8-downloads.html

Maven 3.5+ :http://maven.apache.org/download.cgi

学习文档:

尚硅谷SpringBoot2:https://www.yuque.com/atguigu/springboot/rmxq85

Maven配置

<mirrors>
      <mirror>
        <id>nexus-aliyun</id>
        <mirrorOf>central</mirrorOf>
        <name>Nexus aliyun</name>
        <url>http://maven.aliyun.com/nexus/content/groups/public</url>
      </mirror>
</mirrors>
 
<profiles>
    <profile>
        <id>jdk-1.8</id>
        <activation>
            <activeByDefault>true</activeByDefault>
            <jdk>1.8</jdk>
        </activation>
        <properties>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
            <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
        </properties>
    </profile>
</profiles>

SpringBoot HelloWorld

1、创建maven工程

2、引入依赖

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

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

3、创建主程序

/**
 * 主程序类
 * @SpringBootApplication:这是一个SpringBoot应用
 */
@SpringBootApplication
public class MainApplication {

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

4、编写业务

@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String handle01(){
        return "Hello, Spring Boot 2!";
    }
}

5、直接运行main方法,访问http://localhost:8080/hello

自动配置原理

依赖管理

<!--依赖管理-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
</parent>

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

几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制,如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wb1D8hyh-1627917291679)(./1.png)]

  • 开发导入starter场景启动器
1、见到很多 spring-boot-starter-* : *就某种场景
2、只要引入starter,这个场景的所有常规需要的依赖我们都自动引入
3、SpringBoot所有支持的场景
https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter
4、见到的  *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。
5、所有场景启动器最底层的依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
  <version>2.3.4.RELEASE</version>
  <scope>compile</scope>
</dependency>
  • 无需关注版本号,自动版本仲裁

    • 引入依赖默认都可以不写版本
    • 引入非版本仲裁的jar,要写版本号。
  • 可以修改默认版本号

    • 查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。
    • 在当前项目里面重写配置
    <properties>        
        <mysql.version>5.1.43</mysql.version>    
    </properties>
    

    最基本的依赖入口spring-boot-starter-*(web)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nYyR3hzt-1627917291681)(./2.png)]

自动配置

  • 自动配好Tomcat

    • 引入Tomcat依赖。
    • 配置Tomcat
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <version>2.3.4.RELEASE</version>
        <scope>compile</scope>
    </dependency>
    
  • 自动配好SpringMVC

    • 引入SpringMVC全套组件
    • 自动配好SpringMVC常用组件(功能)
  • 自动配好Web常见功能,如:字符编码问题

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

  • 默认的包结构

    • 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
    • 无需以前的包扫描配置
    • 想要改变扫描路径,@SpringBootApplication(scanBasePackages=“com.ramelon”)
      • 或者@ComponentScan 指定扫描路径
    @SpringBootApplication
    等同于
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan("com.ramelon.boot")
    
  • 各种配置拥有默认值

    • 默认配置最终都是映射到某个类上,如:MultipartProperties
    • 配置文件的值最终会绑定每个类上,这个类会在容器中创建对象
  • 按需加载所有自动配置项

    • 非常多的starter
    • 引入了哪些场景这个场景的自动配置才会开启
    • SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面

容器组件

@Configuration

表明一个类中声明一个和多个@Bean标记的方法,并且这些方法被Spring容器管理用于生成Bean定义以及在运行时这些Bean的服务请求。其实相当于原来的声明了多个bean的xml配置文件,而且被@Configuration也相当于一个组件。

/**
 * 1.配置类里面使用@Bean标注在方法上给容器注册组件,默认单例
 * 2.配置类本身也是组件
 * 3.proxyBeanMethods: 代理bean方法
 *  Full(proxyBeanMethods=true)
 *  Life(proxyBeanMethods=false)
 *  组件依赖必须使用Full默认模式,其他默认是否Lite模式
 *  4、@Import(User.class)
 *  给容器中自动创建出这两个类型的组件,默认组件名字就是全类目名
 *  5、@ConditionalOnBean 条件装配 有了这个bean才生效
 *  6、@ConditionalOnMissingBean  没有这个bean才生效
 *  7、@ImportResource("classpath:beans.xml") 导入spring配置文件
 */

@Import({User.class, DBHelper.class})
@Configuration(proxyBeanMethods = false) // 告诉SpringBoot这是一个配置类==配置文件
@ImportResource("classpath:beans.xml")
// 1.配置绑定功能 2.把指定组件注入到容器中
@EnableConfigurationProperties(Car.class)
public class MyConfig {


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

    @Bean("tom")
    public Cat Tomcat(){
        return new Cat("tomcat");
    }
}

主程序类

/**
 * 主程序类
 * @SpringBootApplication 这是一个springboot应用
 */
@SpringBootApplication
//@SpringBootConfiguration
//@EnableAutoConfiguration
//@ComponentScan("com.ramelon.boot")
public class MainApplication {
    public static void main(String[] args) {
        // 1、返回IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        // 2、查看容器里面的组件
        String[] names = run.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }

        // 3、从容器中获取组件 默认单例
        Cat tom = run.getBean("tom",Cat.class);
        User user01 = run.getBean("user01", User.class);

        System.out.println(tom);

        //4
        MyConfig bean = run.getBean(MyConfig.class);
        System.out.println(bean);

        // @Configuration(proxyBeanMethods == true)
        User user = bean.user01();
        User user1 = bean.user01();
        System.out.println(user == user1);

        // @Configuration(proxyBeanMethods == false)
        User user011 = run.getBean("user01", User.class);
        Cat tom1 = run.getBean("tom", Cat.class);

        System.out.println("用户的宠物" + (user.getCat()== tom1));

        //5 获取组件
        String[] beanNamesForType = run.getBeanNamesForType(User.class);
        System.out.println("=====");
        for (String s : beanNamesForType) {
            System.out.println(s);
        }

        DBHelper bean1 = run.getBean(DBHelper.class);
        System.out.println(bean1);

        boolean haha = run.containsBean("haha");
        boolean hehe = run.containsBean("hehe");
        System.out.println(haha);
        System.out.println(hehe);
    }
}

注意:@Configuration注解的配置类有如下要求:

  1. @Configuration不可以是final类型;
  2. @Configuration不可以是匿名类;
  3. 嵌套的configuration必须是静态类。

一、用@Configuration加载spring
1.1、@Configuration配置spring并启动spring容器
1.2、@Configuration启动容器+@Bean注册Bean
1.3、@Configuration启动容器+@Component注册Bean
1.4、使用 AnnotationConfigApplicationContext 注册 AppContext 类的两种方法
1.5、配置Web应用程序(web.xml中配置AnnotationConfigApplicationContext)

二、组合多个配置类
2.1、在@configuration中引入spring的xml配置文件
2.2、在@configuration中引入其它注解配置
2.3、@configuration嵌套(嵌套的Configuration必须是静态类)
三、@EnableXXX注解
四、@Profile逻辑组配置
五、使用外部变量

@Bean、@Component、@Controller、@Service、@Repository
  • @Bean:表示一个方法实例化、配置或者初始化一个Spring IoC容器管理的新对象。
  • @Component: 自动被comonent扫描。 表示被注解的类会自动被component扫描
  • @Repository: 用于持久层,主要是数据库存储库。
  • @Service: 表示被注解的类是位于业务层的业务component。
  • @Controller:表明被注解的类是控制component,主要用于展现层 。
@ComponentScan、@Import

@ComponentScan用于类或接口上主要是指定扫描路径,spring会把指定路径下带有指定注解的类自动装配到bean容器里。会被自动装配的注解包括@Controller、@Service、@Component、@Repository等等。
其作用等同于<context:component-scan base-package=“com.maple.learn” />配置

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

@ComponentScan("com.ramelon.boot")
@Import({User.class, DBHelper.class})
@Conditional

条件装配:满足Conditional指定的条件,则进行组件注入。 ctrl+h打开继承树。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aMzyoSmi-1627917291682)(./3.png)]

=====================测试条件装配==========================
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
//@ConditionalOnBean(name = "tom")
@ConditionalOnMissingBean(name = "tom")
public class MyConfig {


    /**
     * Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
     * @return
     */

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

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

public static void main(String[] args) {
        //1、返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        //2、查看容器里面的组件
        String[] names = run.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }

        boolean tom = run.containsBean("tom");
        System.out.println("容器中Tom组件:"+tom);

        boolean user01 = run.containsBean("user01");
        System.out.println("容器中user01组件:"+user01);

        boolean tom22 = run.containsBean("tom22");
        System.out.println("容器中tom22组件:"+tom22);	
}

原生配置文件引入

@ImportResource

@ImportResource注解用于导入Spring的配置文件,让配置文件里面的内容生效;(就是以前写的springmvc.xml、applicationContext.xml)
Spring Boot里面没有Spring的配置文件,我们自己编写的配置文件,也不能自动识别;
想让Spring的配置文件生效,加载进来;@ImportResource标注在一个配置类上。
注意!这个注解是放在主入口函数的类上,而不是测试类上

======================beans.xml=========================
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="haha" class="com.atguigu.boot.bean.User">
        <property name="name" value="zhangsan"></property>
        <property name="age" value="18"></property>
    </bean>

    <bean id="hehe" class="com.atguigu.boot.bean.Pet">
        <property name="name" value="tomcat"></property>
    </bean>
</beans>
@ImportResource("classpath:beans.xml")
public class MyConfig {}

======================测试=================
        boolean haha = run.containsBean("haha");
        boolean hehe = run.containsBean("hehe");
        System.out.println("haha:"+haha);//true
        System.out.println("hehe:"+hehe);//true

配置绑定

如何使用Java读取到properties文件中的内容,并且把它封装到JavaBean中,以供随时使用;

原生写法:

public class getProperties {
     public static void main(String[] args) throws FileNotFoundException, IOException {
         Properties pps = new Properties();
         pps.load(new FileInputStream("a.properties"));
         Enumeration enum1 = pps.propertyNames();//得到配置文件的名字
         while(enum1.hasMoreElements()) {
             String strKey = (String) enum1.nextElement();
             String strValue = pps.getProperty(strKey);
             System.out.println(strKey + "=" + strValue);
             //封装到JavaBean。
         }
     }
 }
@Component + @ConfigurationProperties
/**
 * 只有在容器中的组件,才会拥有SpringBoot提供的强大功能
 */
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {

    private String brand;
    private Integer price;

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Car{" +
                "brand='" + brand + '\'' +
                ", price=" + price +
                '}';
    }
}

application.properties

mycar.brand=BYD
mycar.price=100000

controller

@Autowired
Car car;

@RequestMapping("/car")
public Car car(){
    return car;
}

{"brand":"BYD","price":100000}
@EnableConfigurationProperties + @ConfigurationProperties
@ConfigurationProperties(prefix = "mycar")
public class Car {}


// 1.配置绑定功能 2.把指定组件注入到容器中
@EnableConfigurationProperties(Car.class)
public class MyConfig {}

自动配置原理

引导加载自动配置类

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

@SpringBootConfiguration 标注当前类是配置类。

@ComponentScan 指定扫描哪些类。

@EnableAutoConfiguration

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

@AutoConfigurationPackage

// 给容器中导入组件
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {}
//利用Registrar给容器中导入一系列组件
//将指定的一个包下的所有组件导入进来?MainApplication 所在包下。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IzHM6e2a-1627917291684)(./4.png)]

@Import(AutoConfigurationImportSelector.class)

1、利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
2、调用List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类
3、利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件
4、从META-INF/spring.factories位置来加载一个文件。
  默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
    spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories

1、

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-apHTNcsM-1627917291685)(./5.png)]

2、

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EorSmdrI-1627917291686)(./6.png)]

3、

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pybvUTvv-1627917291686)(./7.png)]

4、

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tlMGAVzA-1627917291687)(./8.png)]

按需开启自动配置项

虽然我们130个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration 按照条件装配规则(@Conditional),最终会按需配置。

@ConditionalOnClass()

就是说只有在classpath下能找到Advice类才会构建这个bean。下面代码才会生效,否则不生效。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2X4H9rj6-1627917291688)(./9.png)]

@ConditionalOnMissingClass

bean加载,发现有需求class,优先从已加载里面找,没有就自己找class,找不到class就加载bean。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FAqBpbkX-1627917291688)(./10.png)]

修改默认配置

@Bean
@ConditionalOnBean(MultipartResolver.class)  //容器中有这个类型组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名字 multipartResolver 的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
    //给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
    //SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范
    // Detect if the user has created a MultipartResolver but named it incorrectly
    return resolver;
}
给容器中加入了文件上传解析器;

SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先。

@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
}

总结:

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

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定

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

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

  • 定制化配置

    • 用户直接自己@Bean替换底层的组件

    • 用户去看这个组件是获取的配置文件什么值就去修改。

      server.servlet.encoding.charset=GBK

xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 ----> application.properties

Lombok简化开发

@Data
@NoArgsConstructor
@AllArgsConstructor
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> salarys;
    private Map<String, List<Pet>>allPets;
}
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

dev-tools

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

JRebel

https://www.jianshu.com/p/704b1164a1c1

Spring Initailizr

https://www.cnblogs.com/libingbin/p/10557739.html

核心功能

配置文件

properties文件

后缀properties是一种属性文件。这种文件以key=value格式存储内容。

yml文件

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

@ConfigurationProperties(prefix = "person")
@Component
@Data
@NoArgsConstructor
@AllArgsConstructor
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> salarys;
    private Map<String, List<Pet>>allPets;
}
person:
  boss: true
  birth: 2019/12/9
  age: 18
#  interests: [篮球,足球]
  interests:
   - 篮球
   - 足球
  animal: [狸猫,小白]
#  score: {english:80,math:90}
  score:
   english: 80
   math: 90
  salarys:
   - 9999.88
   - 999.999
  pet:
   name: 小华
   weight: 99.99
  allPets:
   sick:
    - {name: 小华,weight: 99.99}
    - name: 毛毛
      weight: 88.88
   health:
     - {name: 红红,weight: 99.99}
     - {name: 白白,weight: 99.99}
  user-name: zhangsan

语法

  • key: value;kv之间有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • '#'表示注释
  • 字符串无需加引号,如果要加,’'与""表示字符串内容 会被 转义/不转义

配置提示

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

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

Web开发

简单功能分析

静态资源访问

静态资源目录:

只要静态资源放在类路径下: called /static (or /public or /resources or /META-INF/resources
访问 : 当前项目根路径/ + 静态资源名

原理: 静态映射/**。
请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。

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

  resources:
    static-locations: "classpath:/haha/"

静态资源访问前缀

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

当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找

webjars

自动映射 /webjars/**
https://www.webjars.org/

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

访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径

欢迎页支持

  • 静态资源路径下 index.html
    • 可以配置静态资源路径
    • 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
spring:
#  mvc:
#    static-path-pattern: /res/**   这个会导致welcome page功能失效

  resources:
    static-locations: [classpath:/haha/]

自定义 Favicon

favicon.ico 放在静态资源目录下即可。

spring:
#  mvc:
#    static-path-pattern: /res/**   这个会导致 Favicon 功能失效

静态资源配置原理

  • SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类).
  • SpringMVC功能的自动配置类 WebMvcAutoConfiguration,生效.
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
}
  • 给容器中配了什么。
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
}
  • 配置文件的相关属性和xxx进行了绑定。WebMvcProperties==spring.mvc、ResourceProperties==spring.resource
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {}
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {}
配置类只有一个有参构造器
//有参构造器所有参数的值都会从容器中确定
//ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
//WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
//ListableBeanFactory beanFactory Spring的beanFactory
//HttpMessageConverters 找到所有的HttpMessageConverters
//ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========
//DispatcherServletPath  
//ServletRegistrationBean   给应用注册Servlet、Filter....

public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
				ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
				ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
				ObjectProvider<DispatcherServletPath> dispatcherServletPath,
				ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
			this.resourceProperties = resourceProperties;
			this.mvcProperties = mvcProperties;
			this.beanFactory = beanFactory;
			this.messageConvertersProvider = messageConvertersProvider;
			this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
			this.dispatcherServletPath = dispatcherServletPath;
			this.servletRegistrations = servletRegistrations;
		}
资源处理的默认规则
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
    addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(),
                       this.resourceProperties.getStaticLocations());
}
spring:
  resources:
    add-mappings: false   禁用所有静态资源规则

默认CLASSPATH_RESOURCE_LOCATIONS

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {

	private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
			"classpath:/resources/", "classpath:/static/", "classpath:/public/" };

	/**
	 * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
	 * /resources/, /static/, /public/].
	 */
	private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
}
欢迎页的处理规则
// HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
                                                           FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
        new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
        this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
    return welcomePageHandlerMapping;
}
	WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
			ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
		if (welcomePage != null && "/**".equals(staticPathPattern)) {
            //要用欢迎页功能,必须是/**
			logger.info("Adding welcome page: " + welcomePage);
			setRootViewName("forward:index.html");
		}
		else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
			logger.info("Adding welcome page template: index");
			setRootViewName("index");
		}
	}

请求参数处理

请求映射

rest使用与原理

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

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


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

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

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

/** Default method parameter: {@code _method}. */
public static final String DEFAULT_METHOD_PARAM = "_method";

// 自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
    HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
    methodFilter.setMethodParam("_m");
    return methodFilter;
}
<form action="/user" method="get">
    <input value="REST-GET提交" type="submit" />
</form>

<form action="/user" method="post">
    <input value="REST-POST提交" type="submit" />
</form>

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

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

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

  • 表单提交会带上**_method=PUT**

  • 请求过来被HiddenHttpMethodFilter拦截

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

  • 获取到**_method**的值。

  • 兼容以下请求;PUT.DELETE.PATCH

  • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。

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

Rest使用客户端工具

  • 如PostMan直接发送Put、delete等方式请求,无需Filter。
spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true  # springboot rest风格需要手动开启
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {

    HttpServletRequest requestToUse = request;

    if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
        String paramValue = request.getParameter(this.methodParam);
        if (StringUtils.hasLength(paramValue)) {
            String method = paramValue.toUpperCase(Locale.ENGLISH);
            if (ALLOWED_METHODS.contains(method)) {
                requestToUse = new HttpMethodRequestWrapper(request, method);
            }
        }
    }

    filterChain.doFilter(requestToUse, response);
}

允许的请求

private static final List<String> ALLOWED_METHODS =
    Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
                                               HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));

请求映射原理

image.png

SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet-》doDispatch()

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // Determine handler for the current request.
            // 找到当前请求使用哪个Handler(Controller的方法)处理
            mappedHandler = getHandler(processedRequest);
            
            
        }
    }
}


@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yuQFKUdd-1627917291689)(./11.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cz5JB9d1-1627917291690)(.\12.png)]

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

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

  • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping

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

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

  • 如果没有就是下一个 HandlerMapping

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

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

请求处理-常用参数注解使用

@PathVariable 路径变量
@RequestHeader 获取请求头
@RequestParam 获取请求参数
@CookieValue 获取Cookie值
@RequestBody 获取请求体[post]
@RequestAttribute 获取request域属性
@MatrixVariable 矩阵变量

@PathVariable

通过 @PathVariable 可以将URL中占位符参数{xxx}绑定到处理器类的方法形参中@PathVariable(“xxx“)

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

http://localhost:8080/car/3/owner/李四
{pv: {id: "3",username: "李四"},name: "李四",id: 3}
@RequestHeader

用于将请求的头信息区数据映射到功能处理方法的参数上。

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


{
headers: {
host: "localhost:8080",
connection: "keep-alive",
sec-ch-ua: "" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"",
sec-ch-ua-mobile: "?0",
upgrade-insecure-requests: "1",
user-agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
sec-fetch-site: "same-origin",
sec-fetch-mode: "navigate",
sec-fetch-user: "?1",
sec-fetch-dest: "document",
referer: "http://localhost:8080/",
accept-encoding: "gzip, deflate, br",
accept-language: "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
cookie: ""
},
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
@RequestParam

用于将请求参数区数据映射到功能处理方法的参数上。

@GetMapping("/user")
@ResponseBody
public Map<String,Object> getUser(@RequestParam("age") Integer age,
                                 @RequestParam("inters")List<String> inters,
                                 @RequestParam Map<String,Object> params,
                                 @RequestParam MultiValueMap<String,Object> mparams){
    map.put("age",age);
    map.put("inters",inters);
    map.put("params",params);
    map.put("mparams",mparams);
    return map;
}


/car/3/owner/李四?age=18&inters=basketball&inters=game
    
{"mparams":{"age":["18"],"inters":["basketball","game"]},"inters":["basketball","game"],"params":{"age":"18","inters":"basketball"},"age":18}
@CookieValue

用于将请求的Cookie数据映射到功能处理方法的参数上。

@GetMapping("/cookie")
@ResponseBody
public Map<String,Object> getCookie(@CookieValue("_xsrf") String _ga,
                                    @CookieValue("_xsrf") Cookie cookie){
	 HashMap<String, Object> map = new HashMap<>();
     map.put("_ga",_ga);
     map.put("cookie",cookie);
     return map;
}


{"cookie":{"name":"_xsrf","value":"2|ee9fb6d0|a77da24fe979f6979180a8e0bb273d39|1623077601","version":0,"comment":null,"domain":null,"maxAge":-1,"path":null,"secure":false,"httpOnly":false},"_ga":"2|ee9fb6d0|a77da24fe979f6979180a8e0bb273d39|1623077601"}
@RequestBody
@PostMapping("/save")
public Map PostMethod(@RequestBody String content){
    HashMap<String, Object> map = new HashMap<>();
    map.put("content",content);
    return map;
}

<form action="/save" method="post">
    测试 @RequestBody 获取数据
    用户名:<input name="userName"/>
    邮箱:<input name="email"/>
    <input type="submit" value="提交">
</form>
    
提交之后可以拿到userName的值和email的值。
@RequestAttribute

获取HTTP的请求(request)对象属性值,用来传递给控制器的参数。

@GetMapping("/goto")
public String goToPage(HttpServletRequest request){

    request.setAttribute("msg","success");
    request.setAttribute("code","200");
    return "forward:/success";
}

@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute("msg") String msg,
                   @RequestAttribute("code") Integer code,
                   HttpServletRequest request){
    Object msg1 = request.getAttribute("msg");
    request.getAttribute("code");

    HashMap<String, Object> map = new HashMap<>();
    map.put("reqMethod_msg",msg1);
    map.put("annotation_msg",msg);
    return map;
}


访问goto页面;如果成功转发到success页面。
{
    reqMethod_msg: "success",
    annotation_msg: "success"
}
@MatrixVariable

矩阵变量需要在SpringBoot中手动开启
根据RFC3986的规范,矩阵变量应当绑定在路径变量中!
若是有多个矩阵变量,应当使用英文符号**;进行分隔。
若是一个矩阵变量有多个值,应当使用英文符号
,**进行分隔,或之命名多个重复的key即可
如:/cars/sell;low=34;brand=byd,audi,yd

spring.mvc.hiddenmethod.filter.enabled=true

/cars/{path}?xxx=xxx&aaa=ccc queryString 查询字符串。@RequestParam;
/cars/{path;low=34;brand=byd,dudi,yd};矩阵变量
页面开发,cookie禁用了,session里面的内容怎么使用;
session.set(a,b)—>jssessionid—>cookie—>每次发请求携带。
uri重写:/abc;jssessionid=xxxx 把cookie的值使用矩阵变量的方式进行传递。

/boss/1;age=20/2;age=20
/boss/1;age=20/

<a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable</a>
<a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">@MatrixVariable</a>
<a href="/boss/1;age=20/2;age=10">@MatrixVariable</a>
// 1.语法 cars/sell;low=34;brand=byd,audi,yd
// 2.SpringBoot 默认是禁用了矩阵变量的功能
//  手动开启:原理。对于路径的处理。UriPathHelper进行解析。
// removeSemicolonContent(移除分号内容)支持矩阵变量的
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
                    @MatrixVariable("brand") List<String> brand,
                    @PathVariable("path") String path){
    HashMap<String, Object> map = new HashMap<>();
    map.put("low",low);
    map.put("brand",brand);
    map.put("path",path);
    return map;
}

// /boss/1;age=20/2;age=10
@GetMapping("/boss/{bossId}/{empId}")
public Map Boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer boosAge,
                @MatrixVariable(value = "age",pathVar = "empId")Integer empAge){
    HashMap<String, Object> map = new HashMap<>();

    map.put("bossAge",boosAge);
    map.put("empAge",empAge);
    return map;
}


1{path: "sell",low: 34,brand: ["byd","audi","yd"]}
2{"path":"sell","low":34,"brand":["byd","audi","yd"]}
3{"bossAge":20,"empAge":10}

Servlet API

WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId

ServletRequestMethodArgumentResolver 以上的部分参数

@Override
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) ||
         InputStream.class.isAssignableFrom(paramType) ||
         Reader.class.isAssignableFrom(paramType) ||
         HttpMethod.class == paramType ||
         Locale.class == paramType ||
         TimeZone.class == paramType ||
         ZoneId.class == paramType);
}

复杂参数

Map、**Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、**Errors/BindingResult、RedirectAttributes( 重定向携带数据)ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder

Map<String,Object> map,  Model model, HttpServletRequest request 都是可以给request域中放数据,request.getAttribute();

Map、Model类型的参数,会返回 mavContainer.getModel();—> BindingAwareModelMap 是Model 也是Map

mavContainer.getModel(); 获取到值的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wQjt0aaG-1627917291691)(C:\Users\VULCAN\Desktop\md\springboot\18.png)]

自定义对象参数

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

/**
 *     姓名: <input name="userName"/> <br/>
 *     年龄: <input name="age"/> <br/>
 *     生日: <input name="birth"/> <br/>
 *     宠物姓名:<input name="pet.name"/><br/>
 *     宠物年龄:<input name="pet.age"/>
 */
@Data
public class Person {
    
    private String userName;
    private Integer age;
    private Date birth;
    private Pet pet;
    
}

@Data
public class Pet {

    private String name;
    private String age;

}

    <hr/>
    测试原生API
    <a href="testapi">测试原生API</a>
    <hr/>
    测试复杂类型:</hr>
    测试封装POJO:
    <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" name="保存">
    </form>

POJO封装过程

  • ServletModelAttributeMethodProcessor

参数处理原理

  • HandlerMapping中找到能处理请求的Handler(Controller.method())
  • 为当前Handler 找一个适配器 HandlerAdapter; RequestMappingHandlerAdapter
  • 适配器执行目标方法并确定方法参数的每一个值

1、handlerAdapters

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BxG3uULa-1627917291691)(C:\Users\VULCAN\Desktop\md\springboot\13.png)]

0 - 支持方法上标注@RequestMapping
1 - 支持函数式编程的

2、执行目标方法

// Actually invoke the handler.
// DispatcherServlet -- doDispatch
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

// 执行目标方法
mav = invokeHandlerMethod(request, response, handlerMethod);

//ServletInvocableHandlerMethod
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//获取方法的参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);

3、参数解析器-HandlerMethodArgumentResolver

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BAHkvQ0n-1627917291692)(C:\Users\VULCAN\Desktop\md\springboot\14.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-irWIxM50-1627917291692)(C:\Users\VULCAN\Desktop\md\springboot\15.png)]

  • 当前解析器是否支持解析这种参数
  • 支持就调用 resolveArgument

4、返回值处理器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pYv4UVHX-1627917291693)(C:\Users\VULCAN\Desktop\md\springboot\16.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y7FZ2UMe-1627917291693)(C:\Users\VULCAN\Desktop\md\springboot\17.png)]

5、如何确定目标方法每一个参数的值

// InvocableHandlerMethod
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
                                           Object... providedArgs) throws Exception {

    MethodParameter[] parameters = getMethodParameters();
    if (ObjectUtils.isEmpty(parameters)) {
        return EMPTY_ARGS;
    }

    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) {
            continue;
        }
        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 ex) {
            // Leave stack trace for later, exception may actually be resolved and handled...
            if (logger.isDebugEnabled()) {
                String exMsg = ex.getMessage();
                if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
                    logger.debug(formatArgumentError(parameter, exMsg));
                }
            }
            throw ex;
        }
    }
    return args;
}

5.1、挨个判断所有参数解析器那个支持解析这个参数

// HandlerMethodArgumentResolverComposite
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    if (result == null) {
        for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
            if (resolver.supportsParameter(parameter)) {
                result = resolver;
                this.argumentResolverCache.put(parameter, result);
                break;
            }
        }
    }
    return result;
}

5.2、解析这个参数的值

调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可.

5.3、自定义类型参数封装POJO

ServletModelAttributeMethodProcessor 这个参数处理器支持

是否为简单类型。

public static boolean isSimpleValueType(Class<?> type) {
		return (Void.class != type && void.class != type &&
				(ClassUtils.isPrimitiveOrWrapper(type) ||
				Enum.class.isAssignableFrom(type) ||
				CharSequence.class.isAssignableFrom(type) ||
				Number.class.isAssignableFrom(type) ||
				Date.class.isAssignableFrom(type) ||
				Temporal.class.isAssignableFrom(type) ||
				URI.class == type ||
				URL.class == type ||
				Locale.class == type ||
				Class.class == type));
	}
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                                    NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

    Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
    Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");

    String name = ModelFactory.getNameForParameter(parameter);
    ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
    if (ann != null) {
        mavContainer.setBinding(name, ann.binding());
    }

    Object attribute = null;
    BindingResult bindingResult = null;

    if (mavContainer.containsAttribute(name)) {
        attribute = mavContainer.getModel().get(name);
    }
    else {
        // Create attribute instance
        try {
            attribute = createAttribute(name, parameter, binderFactory, webRequest);
        }
        catch (BindException ex) {
            if (isBindExceptionRequired(parameter)) {
                // No BindingResult parameter -> fail with BindException
                throw ex;
            }
            // Otherwise, expose null/empty value and associated BindingResult
            if (parameter.getParameterType() == Optional.class) {
                attribute = Optional.empty();
            }
            bindingResult = ex.getBindingResult();
        }
    }

    if (bindingResult == null) {
        // Bean property binding and validation;
        // skipped in case of binding failure on construction.
        WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
        if (binder.getTarget() != null) {
            if (!mavContainer.isBindingDisabled(name)) {
                bindRequestParameters(binder, webRequest);
            }
            validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                throw new BindException(binder.getBindingResult());
            }
        }
        // Value type adaptation, also covering java.util.Optional
        if (!parameter.getParameterType().isInstance(attribute)) {
            attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
        }
        bindingResult = binder.getBindingResult();
    }

    // Add resolved attribute and BindingResult at the end of the model
    Map<String, Object> bindingResultModel = bindingResult.getModel();
    mavContainer.removeAttributes(bindingResultModel);
    mavContainer.addAllAttributes(bindingResultModel);

    return attribute;
}

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

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

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

GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)

byte – > file

@FunctionalInterfacepublic interface Converter<S, T>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c3AE9hqk-1627917291694)(C:\Users\VULCAN\Desktop\md\springboot\20.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a5z8O1eb-1627917291694)(C:\Users\VULCAN\Desktop\md\springboot\21.jpg)]

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

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

自定义 Converter

//1、WebMvcConfigurer定制化SpringMVC的功能
@Bean
public WebMvcConfigurer webMvcConfigurer(){
    return new WebMvcConfigurer() {
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            UrlPathHelper urlPathHelper = new UrlPathHelper();
            // 不移除;后面的内容。矩阵变量功能就可以生效
            urlPathHelper.setRemoveSemicolonContent(false);
            configurer.setUrlPathHelper(urlPathHelper);
        }

        @Override
        public void addFormatters(FormatterRegistry registry) {
            registry.addConverter(new Converter<String, Pet>() {

                @Override
                public Pet convert(String source) {
                    // 啊猫,3
                    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;
                }
            });
        }
    };
}

6、目标方法执行完成时

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JNONiCXo-1627917291695)(C:\Users\VULCAN\Desktop\md\springboot\19.jpg)]

7、处理派发结果

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

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

InternalResourceView@Override
	protected void renderMergedOutputModel(
			Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

		// Expose the model object as request attributes.
    	// 暴露模型作为请求域属性
		exposeModelAsRequestAttributes(model, request);

		// Expose helpers as request attributes, if any.
		exposeHelpers(request);

		// Determine the path for the request dispatcher.
		String dispatcherPath = prepareForRendering(request, response);

		// Obtain a RequestDispatcher for the target resource (typically a JSP).
		RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
		if (rd == null) {
			throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
					"]: Check that the corresponding file exists within your web application archive!");
		}

		// If already included or response already committed, perform include, else forward.
		if (useInclude(request, response)) {
			response.setContentType(getContentType());
			if (logger.isDebugEnabled()) {
				logger.debug("Including [" + getUrl() + "]");
			}
			rd.include(request, response);
		}

		else {
			// Note: The forwarded resource is supposed to determine the content type itself.
			if (logger.isDebugEnabled()) {
				logger.debug("Forwarding to [" + getUrl() + "]");
			}
			rd.forward(request, response);
		}
	}
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
			HttpServletRequest request) throws Exception {

    //model中的所有数据遍历挨个放在请求域中
		model.forEach((name, value) -> {
			if (value != null) {
				request.setAttribute(name, value);
			}
			else {
				request.removeAttribute(name);
			}
		});
	}

数据响应与内容协商

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhnFtt2Q-1627917291695)(C:\Users\VULCAN\Desktop\md\springboot\29.png)]

响应JSON
<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>2.3.4.RELEASE</version>
    <scope>compile</scope>
</dependency>
返回值解析器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0MkaKA80-1627917291696)(C:\Users\VULCAN\Desktop\md\springboot\22.png)]

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



@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                              ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

    HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
    if (handler == null) {
        throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
    }
    handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}



RequestResponseBodyMethodProcessor  	
@Override
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
			throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

		mavContainer.setRequestHandled(true);
		ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
		ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

		// Try even with null return value. ResponseBodyAdvice could get involved.
        // 使用消息转换器进行写出操作
		writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
	}
返回值解析原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UN6kJfy7-1627917291697)(C:\Users\VULCAN\Desktop\md\springboot\23.png)]

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

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

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

    • \1. 利用 MessageConverters 进行处理 将数据写为json
      • 1、内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
      • 2、服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
      • 3、SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
        • 1、得到MappingJackson2HttpMessageConverter可以将对象写为json
        • 2、利用MappingJackson2HttpMessageConverter将对象转为json再写出去

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xgjbOzsX-1627917291697)(C:\Users\VULCAN\Desktop\md\springboot\24.png)]

SpringMVC到底支持哪些返回值
ModelAndView
Model
View
ResponseEntity 
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask@ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor
HTTPMessageConverter原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SLjVwvLR-1627917291698)(C:\Users\VULCAN\Desktop\md\springboot\25.png)]

HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。
例子:Person对象转为JSON。或者 JSON转为Person

默认的MessageConverter

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-apQG4msi-1627917291698)(C:\Users\VULCAN\Desktop\md\springboot\26.png)]

0 - 只支持Byte类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource.class \ SAXSource.class \ StAXSource.class \StreamSource.class \Source.class
6 - MultiValueMap
7 - true
8 - true
9 - 支持注解方式xml处理的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oU2Kt9Od-1627917291699)(C:\Users\VULCAN\Desktop\md\springboot\27.png)]

内容协商

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

引入xml依赖
 <dependency>
     <groupId>com.fasterxml.jackson.dataformat</groupId>
     <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
postman分别测试返回json和xml

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

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

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

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

发请求: http://localhost:8080/test/person?format=json

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7RgOzI4m-1627917291700)(C:\Users\VULCAN\Desktop\md\springboot\30.png)]

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7TPETjBW-1627917291700)(C:\Users\VULCAN\Desktop\md\springboot\31.png)]

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

内容协商原理
  • 1、判断当前响应头中是否已经有确定的媒体类型。MediaType

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

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KVlVtmVd-1627917291701)(C:\Users\VULCAN\Desktop\md\springboot\32.png)]

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DLGNukSX-1627917291701)(C:\Users\VULCAN\Desktop\md\springboot\33.png)]

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

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

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7MrXzTFf-1627917291702)(C:\Users\VULCAN\Desktop\md\springboot\34.png)]

  • 6、进行内容协商的最佳匹配媒体类型

  • 7、用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M52r0KVu-1627917291702)(C:\Users\VULCAN\Desktop\md\springboot\35.png)]

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

WebMvcConfigurationSupport
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);

if (jackson2XmlPresent) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
		}
自定义 MessageConverter

实现多协议数据兼容。json、xml、x-guigu

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

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

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

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

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

 @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {

            @Override
            public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
				converters.add(new RaMessageConverter());
            }
        }
    }
/**
 * 自动以Converter
 */
public class RaMessageConverter 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);
    }

    /**
     * 服务器要统计所有MessageConverter都能写出哪些内容类型
     * @return
     */
    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return MediaType.parseMediaTypes("application/x-ramelon");
    }

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gz8BdKO1-1627917291703)(C:\Users\VULCAN\Desktop\md\springboot\36.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QeKPwE4l-1627917291703)(C:\Users\VULCAN\Desktop\md\springboot\28.png)]

/**
 * 自定义协商策略
 * @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {

    // Map<String, MediaType> mediaTypes
    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-ramelon"));
    // 指定支持解析哪些参数对应的哪些媒体类型
    ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(mediaTypes);

    configurer.strategies(Arrays.asList(strategy));

}

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

大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】

视图解析与模板引擎

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wi3MsIto-1627917291704)(C:\Users\VULCAN\Desktop\md\springboot\37.png)]

1、视图解析

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

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

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

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

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

    • 1、根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
    • 1、所有的视图解析器尝试是否能根据当前返回值得到View对象
    • 2、得到了 redirect:/main.html --> Thymeleaf new RedirectView()
    • 3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
    • 4、view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作
  • RedirectView 如何渲染【重定向到一个页面】

    • 1、获取目标url地址
    • 2、response.sendRedirect(encodedURL);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yjFFJ66Q-1627917291705)(C:\Users\VULCAN\Desktop\md\springboot\38.png)]

视图解析:

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

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

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

2、基本语法

1、表达式
表达式名字语法用途
变量取值${…}获取请求域、session域、对象等值
选择变量*{…}获取上下文对象值
消息#{…}获取国际化等值
链接@{…}生成链接
片段表达式~{…}jsp:include 作用,引入公共页面片段
2、字面量

文本值: ‘one text’ , ‘Another one!’ **,…**数字: 0 , 34 , 3.0 , 12.3 **,…**布尔值: true , false

空值: null

变量: one,two,… 变量不能有空格

3、文本操作

字符串拼接: +

变量替换: |The name is ${name}|

4、数学运算

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

5、布尔运算

运算符: and , or

一元运算: ! , not

6、比较运算

比较: > , < , >= , <= ( gt , lt , ge , le **)**等式: == , != ( eq , ne )

7、条件运算

If-then: (if) ? (then)

If-then-else: (if) ? (then) : (else)

Default: (value) ?: (defaultvalue)

8、特殊操作

无操作: _

3、设置属性值-th:attr

设置单个值

<form action="subscribe.html" th:attr="action=@{/subscribe}">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
  </fieldset>
</form>

设置多个值

<img src="../../images/gtvglogo.png"  th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

以上两个的代替写法 th:xxxx

<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">

所有h5兼容的标签写法

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes

4、迭代
<tr th:each="prod : ${prods}">
        <td th:text="${prod.name}">Onions</td>
        <td th:text="${prod.price}">2.41</td>
        <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
  <td th:text="${prod.name}">Onions</td>
  <td th:text="${prod.price}">2.41</td>
  <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
5、条件运算
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="#{roles.manager}">User is a manager</p>
  <p th:case="*">User is some other thing</p>
</div>
6、属性优先级
顺序功能属性
1模块包含th:include,th:replace
2模块循环th:each
3条件判断th:if,th:unless,th:switch,th:case
4局部变量th:object,th:with
5通用属性修改th:attr,th:attrprepend,th:attrappend
6特殊属性修改th:value,th:href,th:src…
7文本显示th:text,th:utext
8模块定义th:fragment
9模块移除th:remove

3、thymeleaf使用

引入start
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
自动配置thymeleaf
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {}

自动配好的策略

  • 1、所有thymeleaf的配置值都在 ThymeleafProperties

  • 2、配置好了 SpringTemplateEngine

  • 3、配好了 ThymeleafViewResolver

  • 4、我们只需要直接开发页面

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

public static final String DEFAULT_SUFFIX = ".html";
直接开发
<!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.baidu.com" th:href="${link}">百度</a>
     <a href="www.baidu.com" th:href="@{link}">百度</a>
 </h2>
</body>
</html>

4、构建后台管理系统

项目创建

thymeleaf、web-starter、devtools、lombok

静态资源处理

自动配置好,我们只需要把所有静态资源放到 static 文件夹下

路径构建

th:action="@{/login}

模板抽取

th:insert/replace/include

页面跳转 数据渲染
@Controller
public class IndexController {

    /**
     * 来登录页
     *
     * @return
     */
    @GetMapping(value = {"/", "/login"})
    public String loginPage() {

        return "login";
    }

    @PostMapping("/login")
    public String main(User user, HttpSession session, Model model) {
        if (StringUtils.hasLength(user.getUserName()) && StringUtils.hasLength(user.getPassWord())) {
            // 把登录成功的用户保存起来
            session.setAttribute("loginUser", user);
            return "redirect:/main.html";
        }else {
            model.addAttribute("msg","账号密码错误");
            return "login";
        }
    }

    @GetMapping("/main.html")
    public String mianPage(HttpSession session, Model model) {

        Object loginUser = session.getAttribute("loginUser");
        if(loginUser != null){
            return "main";
        }else {
            model.addAttribute("msg","请重新登录");
            return "login";
        }
    }
}


@Controller
public class TableController {

    @GetMapping("/basic_table")
    public String basic_table(){

        return "table/basic_table";
    }

    @GetMapping("/dynamic_table")
    public String dynamic_table(Model model){

        List<User> users = Arrays.asList(new User("zhangsan","123456"),
                new User("lisi","111222"),
                new User("haha","145632"),
                new User("小溪","888888"));

        model.addAttribute("users",users);
        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";
    }
}
<table class="display table table-bordered" id="hidden-table-info">
    <thead>
        <tr>
            <th>#</th>
            <th>用户名</th>
            <th>密码</th>
        </tr>
    </thead>
    <tbody>
        <tr class="gradeX" th:each="user,stats:${users}">
            <td th:text="${stats.count}">Trident</td>
            <td th:text="${user.userName}">Internet</td>
            <td >[[${user.password}]]</td>
        </tr>
    </tbody>
</table>

拦截器

HandlerInterceptor接口

/**
 * 拦截器
 * 登录检查
 * 1、配置好拦截器要拦截哪些请求
 * 2、把这些配置放在容器中
 */
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 目标方法执行之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String requestURI = request.getRequestURI();

        log.info("拦截的请求路径是{}",requestURI);


        HttpSession session = request.getSession();

        Object loginUser = session.getAttribute("loginUser");

        if(loginUser != null){
            return true;
        }

        request.setAttribute("msg","请先登录");
        // response.sendRedirect("/");
        request.getRequestDispatcher("/").forward(request,response);
        return false;
    }

    /**
     * 目标方法执行以后
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    /**
     * 页面渲染以后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

配置拦截器

/**
 *  1、编写一个拦截器实现HandlerInterceptor接口
 *  2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
 *  3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
 */
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") // 所有请求都被拦截器拦截包括静态资源
                .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**","/favicon.ico"); // 需要放行的请求
    }
}

拦截器原理

  • 根据当前的请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有拦截器】

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

    • 如果当前拦截器preHandler返回为true。则执行下一个拦截器的preHandler
    • 如果当前拦截器返回为false。直接倒叙执行所有已经执行了的拦截器afterCompletion
  • 如果有一个拦截器返回false。直接跳出不执行目标方法

  • 所有拦截器都要返回true。执行目标方法。

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

  • 前面的步骤有异常都会直接出发afterCompletion

  • 页面成功渲染完成以后,也会倒叙出发afterHandler

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ygWija8z-1627917291705)(C:\Users\VULCAN\Desktop\md\springboot\39.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OCTwSmWI-1627917291706)(C:\Users\VULCAN\Desktop\md\springboot\40.png)]

文件上传

<form method="post" action="/upload" enctype="multipart/form-data">
    <input type="file" name="file"><br>
    <input type="submit" value="提交">
</form>
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
                     @RequestParam("username") String username,
                     @RequestParam("headerImg") MultipartFile headerImg,
                     @RequestParam("photos") MultipartFile[] photos) throws IOException {

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

    if(!headerImg.isEmpty()){
        String filename = headerImg.getOriginalFilename();
        headerImg.transferTo(new File("F:\\IMG\\"+filename));
    }

    if (photos.length > 0){
        for (MultipartFile photo : photos) {
            if(!photo.isEmpty()){
                photo.transferTo(new File("F:\\IMG\\"+photo.getOriginalFilename()));
            }
        }
    }
    return "main";
}

自动配置原理

文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties

  • 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
  • 原理步骤
    • 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest**)文件上传请求
    • 2、参数解析器来解析请求中的文件内容封装成MultipartFile
    • 3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>

FileCopyUtils。实现文件流的拷贝

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w5YiAQzg-1627917291706)(C:\Users\VULCAN\Desktop\md\springboot\41.jpg)]

异常处理

1、默认规则

  • 默认情况下,Spring Boot提供/error处理所有错误的映射
  • 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
{
  "timestamp": "2021-07-09T09:54:46.263+00:00",
  "status": 404,
  "error": "Not Found",
  "message": "No message available",
  "path": "/main.htmls"
}

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

要完全替换默认行为,可以实现 ErrorController 并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。
error/下的4xx,5xx页面会被自动解析;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NVIUCcc1-1627917291707)(C:\Users\VULCAN\Desktop\md\springboot\42.png)]

2、定制错误处理逻辑

  • 自定义错误页

    • error/404.html error/5xx.html
    • 有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
      • 底层是 ExceptionHandlerExceptionResolver 支持的
  • @ControllerAdvice+@ExcptionHandler处理异常

    /**
     * 处理整个web controller 的异常
     */
    @ControllerAdvice
    @Slf4j
    public class GlobalExceptionHandler {
    
        @ExceptionHandler({ArithmeticException.class,NullPointerException.class})
        public String handlerArithmeticException(Exception ex){
            log.info("异常是:{}",ex.getMessage());
            return "login";
        }
    
    }
    
  • 实现HandlerExceptionResolver处理异常

    • @ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error

      @ResponseStatus(value = HttpStatus.FORBIDDEN,reason = "用户数量太多")
      public class UserTooManyException extends RuntimeException{
          public UserTooManyException(){
      
          }
      
          public UserTooManyException(String message){
              super(message);
          }
      }
      
  • Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。

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

  • 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则

    @Order(Ordered.HIGHEST_PRECEDENCE) // 优先级
    @Component
    public class CustomHandlerExceptionResolver 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();
        }
    }
    
  • ErrorViewResolver 实现自定义处理异常;

    • response.sendError 。error请求就会转给controller
    • 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
    • basicErrorController 要去的页面地址是 ErrorViewResolver ;

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

ErrorMvcAutoConfiguration自动配置异常处理规则

  • 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {}
  • DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
    Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
    if (Boolean.TRUE.equals(this.includeException)) {
        options = options.including(Include.EXCEPTION);
    }
    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.put("message", "");
    }
    if (!options.isIncluded(Include.BINDING_ERRORS)) {
        errorAttributes.remove("errors");
    }
    return errorAttributes;
}

输入如何封装

@Override
@Deprecated
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
    Map<String, Object> errorAttributes = new LinkedHashMap<>();
    errorAttributes.put("timestamp", new Date());
    addStatus(errorAttributes, webRequest);
    addErrorDetails(errorAttributes, webRequest, includeStackTrace);
    addPath(errorAttributes, webRequest);
    return errorAttributes;
}

private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
    Integer status = getAttribute(requestAttributes, RequestDispatcher.ERROR_STATUS_CODE);
    if (status == null) {
        errorAttributes.put("status", 999);
        errorAttributes.put("error", "None");
        return;
    }
    errorAttributes.put("status", status);
    try {
        errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
    }
    catch (Exception ex) {
        // Unable to obtain a reason
        errorAttributes.put("error", "Http Status " + status);
    }
}
  • 容器中的组件:类型:BasicErrorController-> id:basicErrorController(json+白叶适配)

    • 处理默认/error路径的请求;页面响应new ModelAndView(“error”, model);
    • 容器中有组件 View->id是error;(响应默认错误页)
    • 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。
  • 容器中的组件:类型:DefaultErrorViewResolver-> id:conventionErrorViewResolver

    • 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
      error/404、5xx.html

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

4、异常处理步骤流程

1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException

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

processDispatchResult(processedRequest, response, mappedHandler, mv**, **dispatchException);

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

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fowzHvuM-1627917291708)(C:\Users\VULCAN\Desktop\md\springboot\43.png)]

  • 系统默认的异常解析器;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zPpEQznl-1627917291708)(C:\Users\VULCAN\Desktop\md\springboot\47.png)]

    • DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;

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

      • 如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理

      • 解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gtr2oOwP-1627917291709)(C:\Users\VULCAN\Desktop\md\springboot\45.png)]

      • 默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html

      • 模板引擎最终响应这个页面 error/500.html

这个model里的值就是DefaultErrorAttributes

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vl56UZ5Y-1627917291709)(C:\Users\VULCAN\Desktop\md\springboot\44.png)]

如以下目录未找到error/500.html,modelAndView会返回为null。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ICyXJDwg-1627917291710)(C:\Users\VULCAN\Desktop\md\springboot\46.png)]

如果在map中找到对应的Series.SERVER_ERROR。就会去找5xx的视图然后返回modelAndView。

public enum Series {

    INFORMATIONAL(1),
    SUCCESSFUL(2),
    REDIRECTION(3),
    CLIENT_ERROR(4),
    SERVER_ERROR(5);
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
    ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
    if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
        modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
    }
    return modelAndView;
}
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);
}

Web原生组件注入(Servlet,Filter,Listener)

使用Servlet API

// 效果:直接响应,没有经过Spring的拦截器?
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("6666");
    }
}


@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 request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("MyFilter工作");
        chain.doFilter(request,response);
    }

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

@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监听到项目销毁");
    }
}


// 指定原生Servlet组件都放在那里
@ServletComponentScan(basePackages = "com.ramelon.admin")
@SpringBootApplication
public class Boot05WebAdminApplication {

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

}

扩展:DispatchServlet 如何注册进来

  • 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。

  • 通过 ServletRegistrationBean 把 DispatcherServlet 配置进来。

  • 默认映射的是 / 路径

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gqYvZbmT-1627917291710)(C:\Users\VULCAN\Desktop\md\springboot\48.jpg)]

Tomcat-Servlet;
多个Servlet都能处理到同一层路径,精确优选原则
A: /my/
B: /my/1

加入访问/my/2则是Aservlet,因为B是/my/2,会使用spring的dispatchServlet

使用RegistrationBean

ServletRegistrationBean,FilterRegistrationBean,ServletListenerRegistrationBean

@Configuration
public class MyRegisterConfig {

    @Bean
    public ServletRegistrationBean myServlet(){
        MyServlet myServlet = new MyServlet();

        return new ServletRegistrationBean(myServlet,"/my","/my02");
    }

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

    @Bean
    public ServletListenerRegistrationBean myListener(){
        MyServletContextListener mySwervletContextListener = new MyServletContextListener();
        return new ServletListenerRegistrationBean(mySwervletContextListener);
    }
}

嵌入式Servlet

ServletWebServerApplicationContext

Under the hood, Spring Boot uses a different type of ApplicationContext for embedded servlet container support. The ServletWebServerApplicationContext is a special type of WebApplicationContext that bootstraps itself by searching for a single ServletWebServerFactory bean. Usually a TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory has been auto-configured.

在底层,Spring Boot使用了不同类型的ApplicationContext来支持嵌入式的servlet容器。 ServletWebServerApplicationContext是一种特殊类型的WebApplicationContext,它通过搜索单个ServletWebServerFactory bean来引导自身。 通常,TomcatServletWebServerFactory、JettyServletWebServerFactory或UndertowServletWebServerFactory是自动配置的。  
  • 默认支持的webServer

    • Tomcat, Jetty, or Undertow
    • ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
  • 切换服务器

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DQioz75N-1627917291711)(C:\Users\VULCAN\Desktop\md\springboot\49.png)]

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <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>
    
  • 原理

    • SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
    • web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext`
    • ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(Servlet 的web服务器工厂—> Servlet 的web服务器)
    • SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory
    • 底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
    • ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
    • ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
    • TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize—this.tomcat.start();
    • 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)

定制Servlet容器

  • 实现 WebServerFactoryCustomizer

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

  • 修改配置文件 server.xxx

  • 直接自定义 ConfigurableServletWebServerFactory

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

Spring bean that implements the WebServerFactoryCustomizer interface.  WebServerFactoryCustomizer provides access to the ConfigurableServletWebServerFactory, which includes numerous customization setter methods.  The following example shows programmatically setting the port:

如果需要以编程方式配置嵌入的servlet容器,可以注册一个实现WebServerFactoryCustomizer接口的Spring bean。 WebServerFactoryCustomizer提供了对ConfigurableServletWebServerFactory的访问,其中包括许多定制setter方法。
@Component
public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

    @Override
    public void customize(ConfigurableServletWebServerFactory server) {
        server.setPort(9000);
    }

}
TomcatServletWebServerFactory, JettyServletWebServerFactory and UndertowServletWebServerFactory are dedicated variants of ConfigurableServletWebServerFactory that have additional customization setter methods for Tomcat, Jetty and Undertow respectively.  The following example shows how to customize TomcatServletWebServerFactory that provides access to Tomcat-specific configuration options:

TomcatServletWebServerFactory、JettyServletWebServerFactory和UndertowServletWebServerFactory是ConfigurableServletWebServerFactory的专用变体,它们分别为Tomcat、Jetty和Undertow提供了额外的定制setter方法。 下面的例子展示了如何自定义TomcatServletWebServerFactory,它提供了对特定于tomcat的配置选项的访问:  
import java.time.Duration;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

@Component
public class MyTomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory server) {
        server.addConnectorCustomizers((connector) -> connector.setAsyncTimeout(Duration.ofSeconds(20).toMillis()));
    }
}

定制化原理

定制化的常见方式

  • 修改配置文件;
  • xxxxxCustomizer;
  • 编写自定义的配置类 xxxConfiguration;+ @Bean替换、增加容器中默认组件;视图解析器
  • Web应用 编写一个配置类实现** WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{}
  • @EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能
    • 原理
    • 1、WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。静态资源、欢迎页…
    • 2、一旦使用 @EnableWebMvc 、。会 @Import(DelegatingWebMvcConfiguration.class)
    • 3、DelegatingWebMvcConfiguration 的 作用,只保证SpringMVC最基本的使用
      • 把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
      • 自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
      • public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
    • 4、WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    • 5、@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。

原理分析套路

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

数据访问

SQL

数据源的自动配置HikariDataSource

<!--jdbc场景依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
数据驱动
<!--默认版本:-->
<mysql.version>8.0.22</mysql.version>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <!--<version>5.1.49</version>-->
</dependency>

<!--想要修改版本
1、直接依赖引入具体版本(maven的就近依赖原则)
2、重新声明版本(maven的属性的就近优先原则)-->
<properties>
    <java.version>1.8</java.version>
    <mysql.version>5.1.49</mysql.version>
</properties>
分析自动配置
  • DataSourceAutoConfiguration : 数据源的自动配置

    • 修改数据源相关的配置:spring.datasource
    @ConfigurationProperties(prefix = "spring.datasource")
    public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {}
    
    • 数据库连接池的配置,是自己容器中没有DataSource才自动配置的
    	@Configuration(proxyBeanMethods = false)
    	@Conditional(PooledDataSourceCondition.class)
    	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    	@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
    			DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
    			DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
    	protected static class PooledDataSourceConfiguration {
    
    	}
    
    • 底层配置好的连接池是:HikariDataSource
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(HikariDataSource.class)
    @ConditionalOnMissingBean(DataSource.class)
    @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
                           matchIfMissing = true)
    static class Hikari {
    
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.hikari")
        HikariDataSource dataSource(DataSourceProperties properties) {
            HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
            if (StringUtils.hasText(properties.getName())) {
                dataSource.setPoolName(properties.getName());
            }
            return dataSource;
        }
    
    }
    
  • DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置

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

    //jdbc配置项
    @ConfigurationProperties(prefix = "spring.jdbc")
    public class JdbcProperties {}
    
    // JdbcTemplate;容器中有这个组件  使用@Autowired
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(JdbcOperations.class)
    class JdbcTemplateConfiguration {
    
    	@Bean
    	@Primary
    	JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
    		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    		JdbcProperties.Template template = properties.getTemplate();
    		jdbcTemplate.setFetchSize(template.getFetchSize());
    		jdbcTemplate.setMaxRows(template.getMaxRows());
    		if (template.getQueryTimeout() != null) {
    			jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds());
    		}
    		return jdbcTemplate;
    	}
    }
    
  • JndiDataSourceAutoConfiguration: jndi的自动配置

  • XADataSourceAutoConfiguration: 分布式事务相关的

修改配置项
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
    #    type: com.zaxxer.hikari.HikariDataSource
  jdbc:
    template:
      query-timeout: 3
测试
@SpringBootTest
@Slf4j
class Boot05WebAdminApplicationTests {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() {
        List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from poil");
       log.info("{}",maps);
//        System.out.println(maps);
        log.info("数据源类型:{}",dataSource.getClass());
    }

}

使用Druid数据源

Druid

https://github.com/alibaba/druid

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
</dependency>
自定义方式
@Configuration
public class MyDataSourceConfig {

    // 默认自动配置判断容器中没有才会配 @ConditionalOnMissingBean(DataSource.class) HikariDataSource
    @ConfigurationProperties("spring.datasource")
    @Bean
    public DataSource dataSource() throws SQLException {
        DruidDataSource dataSource = new DruidDataSource();
        //        dataSource.setUrl();
        //        dataSource.setUsername();
        //        dataSource.setPassword();
        //        dataSource.setDriver();
        // 加入监控
        dataSource.setFilters("stat,wall");
        return dataSource;
    }

    /**
     * 配置durid的监控页面
     * @return
     */
    @Bean
    public ServletRegistrationBean statViewServlet(){

        StatViewServlet statViewServlet = new StatViewServlet();
        ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");

        registrationBean.addInitParameter("loginUsername","admin");
        registrationBean.addInitParameter("loginPassword","123456");
        return registrationBean;
    }

    // WebStatFilter用于采集web-jdbc关联监控的数据。
    @Bean
    public FilterRegistrationBean webStatFilter(){
        WebStatFilter webStatFilter = new WebStatFilter();
        FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
        filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");

        return filterRegistrationBean;
    }

    // spring监控
    @Bean
    public DruidStatInterceptor druidStatInterceptor() {
        DruidStatInterceptor dsInterceptor = new DruidStatInterceptor();
        return dsInterceptor;
    }

    // aop定义切点
    @Bean
    @Scope("prototype")
    public JdkRegexpMethodPointcut druidStatPointcut() {
        JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
        pointcut.setPattern("com.ramelon.admin.controller.*");
        return pointcut;
    }

    // Springboot的切点可配置化
    @Bean
    public DefaultPointcutAdvisor druidStatAdvisor(DruidStatInterceptor druidStatInterceptor, JdkRegexpMethodPointcut druidStatPointcut) {
        DefaultPointcutAdvisor defaultPointAdvisor = new DefaultPointcutAdvisor();
        defaultPointAdvisor.setPointcut(druidStatPointcut);
        defaultPointAdvisor.setAdvice(druidStatInterceptor);
        return defaultPointAdvisor;
    }
}
使用官方starter方式
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.17</version>
</dependency>
分析自动配置
  • DruidSpringAopConfiguration.class

    spring监控功能

    @ConditionalOnProperty("spring.datasource.druid.aop-patterns")
    
  • DruidStatViewServletConfiguration.class

    配置页配置

    @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true")
    
  • DruidWebStatFilterConfiguration.class
    web监控配置

@ConditionalOnProperty(name = "spring.datasource.druid.web-stat-filter.enabled", havingValue = "true")
  • DruidFilterConfiguration.class
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
private static final String FILTER_WALL_CONFIG_PREFIX = FILTER_WALL_PREFIX + ".config";
配置示例
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
    #    type: com.zaxxer.hikari.HikariDataSource
    druid:
      stat-view-servlet: # 配置监控页功能
        enabled: true
        login-password: 123456
        login-username: admin
        reset-enable: false
      web-stat-filter: # 监控web
        enabled: true
        url-pattern: "/*"
        exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"

      filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙)
      aop-patterns: com.ramelon.admin.* #监控Spring
      filter: # 对上面filters里面的stat的详细配置
        stat:
          slow-sql-millis: 1000
          log-slow-sql: true
          enabled: true
        wall:
          enabled: true
          config:
            update-allow: false
            drop-table-allow: false

官方文档参考配置

https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

DruidDataSource配置属性列表

https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8

整合MyBatis操作

https://github.com/mybatis

SpringBoot官方的Starter:spring-boot-starter-*
第三方的: *-spring-boot-starter

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>
配置模式

全局配置文件

@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {}
  • SqlSessionFactory: 自动配置好了

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {}
    
  • SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        ExecutorType executorType = this.properties.getExecutorType();
        if (executorType != null) {
            return new SqlSessionTemplate(sqlSessionFactory, executorType);
        } else {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }
    
  • @Import(AutoConfiguredMapperScannerRegistrar.class);

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

# 配置mybatis规则
mybatis:
  # config-location: "classpath:mybatis/mybatis-config.xml"
  mapper-locations: "classpath:mybatis/mapper/*.xml"
  configuration:
    map-underscore-to-camel-case: true
# 注意:
# Property 'configuration' and 'configLocation' can not specified with together
# 可以不写全局;配置文件,所有全局配置文件的配置都放在configuration配置项中即可
<!--mapper文件-->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ramelon.admin.mapper.OrdersMapper">

    <!--public Orders getOrderById(String id);-->
    <select id="getOrderById" parameterType="java.lang.String" resultType="com.ramelon.admin.bean.Orders">
        select * from orders where id = ${id}
    </select>

</mapper>

步骤

  • 导入mybatis官方starter
  • 编写mapper接口。标准@Mapper注解
  • 编写sql映射文件并绑定mapper接口
  • 在application.yaml中指定Mapper配置文件的位置,以及指定全局配置文件的信息 (建议;配置在mybatis.configuration)
注解模式

https://github.com/mybatis/spring-boot-starter/wiki/Quick-Start

@Mapper
public interface CityMapper {

    @Select("select * from city where id = #{id}")
    City getById(Long id);

    @Insert("insert into city (`name`,`state`,`country`) values (#{name},#{state},#{country})")
    @Options(useGeneratedKeys=true,keyProperty="id")
    void insertCity(City city);
}
混合模式

有需要的话可以同时使用注解或者mapper.xml一起使用。

最佳实战:

  • 引入mybatis-starter
  • 配置application.yaml中,指定mapper-location位置即可
  • 编写Mapper接口并标注@Mapper注解
  • 简单方法直接注解方式
  • 复杂方法编写mapper.xml进行绑定映射
  • @MapperScan(“com.atguigu.admin.mapper”) 简化,其他的接口就可以不用标注@Mapper注解

整合MyBatis-Plus完成CRUD

MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
依赖
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.1</version>
</dependency>
demo
DROP TABLE IF EXISTS user;

CREATE TABLE user
(
	id BIGINT(20) NOT NULL COMMENT '主键ID',
	name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
	age INT(11) NULL DEFAULT NULL COMMENT '年龄',
	email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
	PRIMARY KEY (id)
);

DELETE FROM user;

INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "user")
public class DUser {

    // @TableField(exist = false) 代表某个字段在数据库中不存在
    private Long id;
    private String name;
    private Integer age;
    private String email;
}


public interface DUserMapper extends BaseMapper<DUser> {
}


public interface DUserService extends IService<DUser>{
}

@Service
public class DUserServiceImpl extends ServiceImpl<DUserMapper, DUser> implements DUserService {

}

@Autowired
DUserService dUserService;

@Test
void testUserMapper(){
    DUser dUser = dUserService.getById(1);
    log.info("用户信息=={}",dUser);
}

/* 当实现类唯一时,表面上注入接口,其实注入实现类
当实现类多个时,通过@Service(“menuService1”)指定实现类
解析:这个其实是创建了实现类的对象但引用了接口类型,即 “InjectionDao injectionDao = new InjectionDaoImpl ()”, 是 Java 多态性(向上转型)的一种应用。在实现类处加 @Repository 注解,意思就是 new InjectionDaoImpl (),而在 InjectionServiceImpl 中定义属性 InjectionDAO injectionDAO 就是将 new 出来的这个 InjectionDaoImpl 对象向上转型为 InjectionDao 类型。若一个接口被多个实现类,实现的时候,@Autowired,Spring 会按 byType 的方式寻找接口的实现类,将其注入。存在多个实现类,应该指定名字,可以通过 byName 注入的方式。可以使用 @Resource 或 @Qualifier 注解。 */
    
//实现类1
@Service("menuService1")
public class MenuServiceImpl implements IMenuService
 
//实现类2
@Service("menuService2")
public class MenuServiceImpl implements IMenuService
 
//注入接口,相当于new一个实现类,指定名称menuService1实现类
@Autowired
@Qualifier("menuService1")
private IMenuService menuService;

//注入接口,相当于new一个实现类,指定名称menuService2实现类  
@Autowired
@Qualifier("menuService2")
private IMenuService menuService;

NoSQL

Redis自动配置

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

自动配置:

  • RedisAutoConfiguration 自动配置类。RedisProperties 属性类 --> spring.redis.xxx是对redis的配置
  • 连接工厂是准备好的。LettuceConnectionConfiguration、JedisConnectionConfiguration
  • 自动注入了RedisTemplate<Object, Object> : xxxTemplate;
  • 自动注入了StringRedisTemplate;k:v都是String
  • key:value
  • 底层只要我们使用 StringRedisTemplate、RedisTemplate就可以操作redis

RedisTemplate和Lettuce

@Test
void testRedis(){
    ValueOperations<String, String> operations = redisTemplate.opsForValue();
    operations.set("hello","world");
    String hello = operations.get("hello");
    System.out.println(hello);
}

Jedis

<!--依赖-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

application.yml

spring:
   redis:
    # url: "redis://mary:Xzy123456@r-bp1rzclsyehzcxocxxpd.redis.rds.aliyuncs.com:6379"
    host: r-bp1rzclsyehzcxocxxpd.redis.rds.aliyuncs.com
    port: 6379
    password: mary:Xzy123456
    client-type: jedis
    jedis:
      pool:
        max-active: 10
#    lettuce:
#      pool:
#        max-active: 10
#        min-idle: 5
#

单元测试

JUnit5

Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库。
作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。
JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。

注意:
SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)
JUnit 5’s Vintage Engine Removed from spring-boot-starter-test,如果需要继续兼容junit4需要自行引入vintage.

<dependency>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

springboot测试依赖:

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

JUnit5常用注解

JUnit5的注解与JUnit4的注解有所变化
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

@Test :表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
@ParameterizedTest :表示方法是参数化测试,下方会有详细介绍
@RepeatedTest :表示方法可重复执行,下方会有详细介绍
@DisplayName :为测试类或者测试方法设置展示名称
@BeforeEach :表示在每个单元测试之前执行
@AfterEach :表示在每个单元测试之后执行
@BeforeAll :表示在所有单元测试之前执行
@AfterAll :表示在所有单元测试之后执行
@Tag :表示单元测试类别,类似于JUnit4中的@Categories
@Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
@Timeout :表示测试方法运行如果超过了指定时间将会返回错误
@ExtendWith :为测试类或测试方法提供扩展类引用
@SpringBootTest
@DisplayName("Junit功能测试类")
public class JunitTest {

    @Autowired
    JdbcTemplate jdbcTemplate;

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

    @Test
    @Disabled
    void test2(){
        System.out.println(2);
    }

    @RepeatedTest(5)
    @Test
    void test3(){
        System.out.println(5);
    }

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

    @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("所有测试已经结束了。。");
    }
}

断言

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告;

https://junit.org/junit5/docs/current/user-guide/#writing-tests-assertions

方法说明
assertEquals判断两个对象或两个原始类型是否相等
assertNotEquals判断两个对象或两个原始类型是否不相等
assertSame判断两个对象引用是否指向同一个对象
assertNotSame判断两个对象引用是否指向不同的对象
assertTrue判断给定的布尔值是否为 true
assertFalse判断给定的布尔值是否为 false
assertNull判断给定的对象引用是否为 null
assertNotNull判断给定的对象引用是否不为 null
@DisplayName("测试简单断言")
@Test
void testSimpleAssertions(){
    int cal = calculate(3,2);
    assertEquals(5,cal,"业务逻辑计算失败");

    Object o = new Object();
    Object o2 = new Object();

    assertSame(o,o2,"两个对象不一样");

}

int calculate(int i,int j){
    return i+j;
}

// 数组断言
@Test
@DisplayName("array assertion")
void testArray() {
    assertArrayEquals(new int[]{2, 1}, new int[] {1, 2});
}

// 组合断言
@Test
@DisplayName("assert all")
void all() {
    assertAll("Math",
              () -> assertEquals(2, 1 + 1,"两个数不想等"),
              () -> assertTrue(1 > 0,"比较错误")
             );
}


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

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

// 快速失败
@Test
void testFil(){
    if(true){
        fail("测试失败");
    }
}

前置条件

JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

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

assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。

assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。

@DisplayName("测试前置条件")
@Test
void testAssumptions(){
    Assumptions.assumeTrue(false,"结果不为true");

    System.out.println("!111");
}

官方实例

class AssumptionsDemo {

    private final Calculator calculator = new Calculator();

    @Test
    void testOnlyOnCiServer() {
        assumeTrue("CI".equals(System.getenv("ENV")));
        // remainder of test
    }

    @Test
    void testOnlyOnDeveloperWorkstation() {
        assumeTrue("DEV".equals(System.getenv("ENV")),
            () -> "Aborting test: not on developer workstation");
        // remainder of test
    }

    @Test
    void testInAllEnvironments() {
        assumingThat("CI".equals(System.getenv("ENV")),
            () -> {
                // perform these assertions only on the CI server
                assertEquals(2, calculator.divide(4, 2));
            });

        // perform these assertions in all environments
        assertEquals(42, calculator.multiply(6, 7));
    }

}

嵌套测试

JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。

// 官方测试实例
@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, stack::pop);
        }

        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, stack::peek);
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

嵌套测试情况下,外层的Test不能驱动内层@BeforeEach等等。。

@DisplayName("嵌套测试")
public class NestedTest {

    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不能驱动内层@BeforeEach等等。。
            assertNotNull(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());
            }
        }
    }


}

参数化测试

https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests

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

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

@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

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

// 参数化测试
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
    System.out.println(string);
    Assertions.assertTrue(StringUtils.isNotBlank(string));
}


@ParameterizedTest
@MethodSource("method")    //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
    System.out.println(name);
    Assertions.assertNotNull(name);
}

static Stream<String> method() {
    return Stream.of("apple", "banana");
}


迁移指南

https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4

在进行迁移的时候需要注意如下的变化:
注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
把@Before 和@After 替换成@BeforeEach 和@AfterEach。
把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。
把@Ignore 替换成@Disabled。
把@Category 替换成@Tag。
把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。

指标监控

SpringBoot Actuator

SpringBoot自带监控功能Actuator,可以帮助实现对程序内部运行情况监控,比如监控状况、Bean加载情况、环境变量、日志信息、线程信息等。

https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator

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

如何使用:

1、引入依赖

2、http://localhost:8080/actuator/**

3、暴露所有监控信息

management:
  endpoints:
    enabled-by-default: true #暴露所有端点信息
    web:
      exposure:
        include: '*'  #以web方式暴露

可视化:

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

https://codecentric.github.io/spring-boot-admin/2.4.3/#getting-started

1、引入依赖:

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
    <version>2.4.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2、Pull in the Spring Boot Admin Server configuration via adding @EnableAdminServer to your configuration:

@Configuration
@EnableAutoConfiguration
@EnableAdminServer
public class SpringBootAdminApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootAdminApplication.class, args);
    }
}

3、导入Spring Boot Admin Client

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-client</artifactId>
    <version>2.4.3</version>
</dependency>

4、yml文件:

spring.boot.admin.client.url=http://localhost:8080  
management.endpoints.web.exposure.include=*

Actuator Endpoint

常用端点:

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

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

IDDescription
heapdump返回hprof堆转储文件。
jolokia通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
logfile返回日志文件的内容(如果已设置logging.file.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheusprometheus

最常用的Endpoint

  • Health:监控状况
  • Metrics:运行时指标
  • Loggers:日志记录

Health Endpoint

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

  • health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
  • 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
  • 可以很容易的添加自定义的健康检查机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ltmfOnEp-1627917291712)(C:\Users\VULCAN\Desktop\md\springboot\50.png)]

Metrics Endpoint

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

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

  • 简化核心Metrics开发

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

管理Endpoints

开启:

management:
  endpoint:
    beans:
      enabled: true

**禁用:**禁用所有的Endpoint然后手动开启指定的Endpoint

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

暴露Endpoints

支持的暴露方式
HTTP:默认只暴露health和info Endpoint
JMX:默认暴露所有Endpoint
除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则

IDJMXWeb
auditeventsYesNo
beansYesNo
cachesYesNo
conditionsYesNo
configpropsYesNo
envYesNo
flywayYesNo
healthYesYes
heapdumpN/ANo
httptraceYesNo
infoYesNo
integrationgraphYesNo
jolokiaN/ANo
logfileN/ANo
loggersYesNo
liquibaseYesNo
metricsYesNo
mappingsYesNo
prometheusN/ANo
quartzYesNo
scheduledtasksYesNo
sessionsYesNo
shutdownYesNo
startupYesNo
threaddumpYesNo

要更改公开哪些端点,请使用以下特定于技术的’ include ‘和’ exclude '属性:

PropertyDefault
management.endpoints.jmx.exposure.exclude
management.endpoints.jmx.exposure.include*
management.endpoints.web.exposure.exclude
management.endpoints.web.exposure.includehealth

定制Endpoint

定制Health信息

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class MyHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        int errorCode = check();
        if (errorCode != 0) {
            return Health.down().withDetail("Error Code", errorCode).build();
        }
        return Health.up().build();
    }

    private int check() {
        // perform some specific health check
        return ...
    }

}
management:
    health:
      enabled: true
      show-details: always #总是显示详细信息。可显示每个模块的状态信息
@Component
public class MyComponentHealthIndicator extends AbstractHealthIndicator {

    /**
     * 真实的检查方法
     * @param builder
     * @throws Exception
     */
    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        // mongdb 获取链接进行测试
        Map<String, Object> map = new HashMap<>();
        if(true){
            // builder.up();
            builder.status(Status.UP);
            map.put("count",1);
            map.put("ms",100);
        }else {
            // builder.down();
            builder.status(Status.OUT_OF_SERVICE);
            map.put("err","链接超时");
            map.put("ms",3000);
        }

        builder.withDetail("code",100)
                .withDetails(map);
    }
}

定制info信息

info:
  appName: boot-admin
  appVersion: 1.0.0
  mavenProjectName: @project.artifactId@
  mavenProjectVersion: @project.version@
@Component
public class AppInfoContributor implements InfoContributor {
    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("msg","你好")
                .withDetail("hello","pop")
                .withDetails(Collections.singletonMap("world","6666666600000"));
    }
}

定制Metrics信息

class MyService{
    Counter counter;
    public MyService(MeterRegistry meterRegistry){
         counter = meterRegistry.counter("myservice.method.running.counter");
    }

    public void hello() {
        counter.increment();
    }
}


//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {
    return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}

定制Endpoint

@Component
@Endpoint(id = "container")
public class DockerEndpoint {


    @ReadOperation
    public Map getDockerInfo(){
        return Collections.singletonMap("info","docker started...");
    }

    @WriteOperation
    private void restartDocker(){
        System.out.println("docker restarted....");
    }

}

原理解析

此处未很明白,待弄清楚回来写心得。

1、Profile功能

为了方便多环境适配,springboot简化了profile功能。

1、application-profile功能

  • 默认配置文件 application.yaml;任何时候都会加载

  • 指定环境配置文件 application-{env}.yaml

  • 激活指定环境

  • 配置文件激活

  • 命令行激活:java -jar xxx.jar –spring.profiles.active=prod --person.name=haha

  • 修改配置文件的任意值,命令行优先

  • 默认配置与环境配置同时生效

  • 同名配置项,profile配置优先

2、@Profile条件装配功能

@Configuration(proxyBeanMethods = false)
@Profile("production")
public class ProductionConfiguration {

    // ...

}

3、profile分组

spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq

使用:--spring.profiles.active=production  激活

2、外部化配置

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config

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

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

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

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

  5. OS environment variables.

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

  7. JNDI attributes from java:comp/env.

  8. ServletContext init parameters.

  9. ServletConfig init parameters.

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

  11. Command line arguments.

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

  13. @TestPropertySource annotations on your tests.

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

1、外部配置源

常用:Java属性文件YAML文件环境变量命令行参数

2、配置文件查找位置

(1) classpath 根路径

(2) classpath 根路径下config目录

(3) jar包当前目录

(4) jar包当前目录的config目录

(5) /config子目录的直接子目录

3、配置文件加载顺序:

  1. 当前jar包内部的application.properties和application.yml

  2. 当前jar包内部的application-{profile}.properties 和 application-{profile}.yml

  3. 引用的外部jar包的application.properties和application.yml

  4. 引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml

4、指定环境优先,外部优先,后面的可以覆盖前面的同名配置项

3、自定义starter

1、starter启动原理

  • starter-pom引入 autoconfigurer 包

img

  • autoconfigure包中配置使用 META-INF/spring.factoriesEnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类

  • 编写自动配置类 xxxAutoConfiguration -> xxxxProperties

  • @Configuration

  • @Conditional

  • @EnableConfigurationProperties

  • @Bean

引入starter — xxxAutoConfiguration — 容器中放入组件 ---- 绑定xxxProperties ---- 配置项

2、自定义starter

atguigu-hello-spring-boot-starter(启动器)

atguigu-hello-spring-boot-starter-autoconfigure(自动配置包)

4、SpringBoot原理

https://www.cnblogs.com/shamo89/p/8184960.html

Spring原理【Spring注解】、SpringMVC原理、自动配置原理、SpringBoot原理

1、SpringBoot启动过程

  • 创建 SpringApplication

  • 保存一些信息。

  • 判定当前应用的类型。ClassUtils。Servlet

  • bootstrappers**:初始启动引导器(List):去spring.factories文件中找** org.springframework.boot.Bootstrapper

  • ApplicationContextInitializer;去spring.factories****找 ApplicationContextInitializer

  • List<ApplicationContextInitializer<?>> initializers

  • ApplicationListener ;应用监听器。spring.factories****找 ApplicationListener

  • List<ApplicationListener<?>> listeners

  • 运行 SpringApplication

  • StopWatch

  • 记录应用的启动时间

  • **创建引导上下文(Context环境)**createBootstrapContext()

  • 获取到所有之前的 bootstrappers 挨个执行 intitialize() 来完成对引导启动器上下文环境设置

  • 让当前应用进入headless模式。java.awt.headless

  • 获取所有 RunListener**(运行监听器)【为了方便所有Listener进行事件感知】**

  • getSpringFactoriesInstances 去spring.factories****找 SpringApplicationRunListener.

  • 遍历 SpringApplicationRunListener 调用 starting 方法;

  • 相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting。

  • 保存命令行参数;ApplicationArguments

  • 准备环境 prepareEnvironment();

  • 返回或者创建基础环境信息对象。StandardServletEnvironment

  • 配置环境信息对象。

  • 读取所有的配置源的配置属性值。

  • 绑定环境信息

  • 监听器调用 listener.environmentPrepared();通知所有的监听器当前环境准备完成

  • 创建IOC容器(createApplicationContext())

  • 根据项目类型(Servlet)创建容器,

  • 当前会创建 AnnotationConfigServletWebServerApplicationContext

  • 准备ApplicationContext IOC容器的基本信息 prepareContext()

  • 保存环境信息

  • IOC容器的后置处理流程。

  • 应用初始化器;applyInitializers;

  • 遍历所有的 ApplicationContextInitializer 。调用 initialize.。来对ioc容器进行初始化扩展功能

  • 遍历所有的 listener 调用 contextPrepared。EventPublishRunListenr;通知所有的监听器****contextPrepared

  • 所有的监听器 调用 contextLoaded。通知所有的监听器 contextLoaded;

  • **刷新IOC容器。**refreshContext

  • 创建容器中的所有组件(Spring注解)

  • 容器刷新完成后工作?afterRefresh

  • 所有监听 器 调用 listeners.started(context); 通知所有的监听器 started

  • **调用所有runners;**callRunners()

  • 获取容器中的 ApplicationRunner

  • 获取容器中的 CommandLineRunner

  • 合并所有runner并且按照@Order进行排序

  • 遍历所有的runner。调用 run 方法

  • 如果以上有异常,

  • 调用Listener 的 failed

  • 调用所有监听器的 running 方法 listeners.running(context); 通知所有的监听器 running

  • **running如果有问题。继续通知 failed 。**调用所有 Listener 的 **failed;**通知所有的监听器 failed

public interface Bootstrapper {

	/**
	 * Initialize the given {@link BootstrapRegistry} with any required registrations.
	 * @param registry the registry to initialize
	 */
	void intitialize(BootstrapRegistry registry);

}
@FunctionalInterface
public interface ApplicationRunner {

	/**
	 * Callback used to run the bean.
	 * @param args incoming application arguments
	 * @throws Exception on error
	 */
	void run(ApplicationArguments args) throws Exception;

}
@FunctionalInterface
public interface CommandLineRunner {

	/**
	 * Callback used to run the bean.
	 * @param args incoming main method arguments
	 * @throws Exception on error
	 */
	void run(String... args) throws Exception;

}

2、Application Events and Listeners

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-application-events-and-listeners

ApplicationContextInitializer

ApplicationListener

SpringApplicationRunListener

3、ApplicationRunner 与 CommandLineRunner

in an environment variable or system property).

  1. Command line arguments.

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

  3. @TestPropertySource annotations on your tests.

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

1、外部配置源

常用:Java属性文件YAML文件环境变量命令行参数

2、配置文件查找位置

(1) classpath 根路径

(2) classpath 根路径下config目录

(3) jar包当前目录

(4) jar包当前目录的config目录

(5) /config子目录的直接子目录

3、配置文件加载顺序:

  1. 当前jar包内部的application.properties和application.yml

  2. 当前jar包内部的application-{profile}.properties 和 application-{profile}.yml

  3. 引用的外部jar包的application.properties和application.yml

  4. 引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml

4、指定环境优先,外部优先,后面的可以覆盖前面的同名配置项

3、自定义starter

1、starter启动原理

  • starter-pom引入 autoconfigurer 包

[外链图片转存中…(img-KxWZ3zpm-1627917291713)]

  • autoconfigure包中配置使用 META-INF/spring.factoriesEnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类

  • 编写自动配置类 xxxAutoConfiguration -> xxxxProperties

  • @Configuration

  • @Conditional

  • @EnableConfigurationProperties

  • @Bean

引入starter — xxxAutoConfiguration — 容器中放入组件 ---- 绑定xxxProperties ---- 配置项

2、自定义starter

atguigu-hello-spring-boot-starter(启动器)

atguigu-hello-spring-boot-starter-autoconfigure(自动配置包)

4、SpringBoot原理

https://www.cnblogs.com/shamo89/p/8184960.html

Spring原理【Spring注解】、SpringMVC原理、自动配置原理、SpringBoot原理

1、SpringBoot启动过程

  • 创建 SpringApplication

  • 保存一些信息。

  • 判定当前应用的类型。ClassUtils。Servlet

  • bootstrappers**:初始启动引导器(List):去spring.factories文件中找** org.springframework.boot.Bootstrapper

  • ApplicationContextInitializer;去spring.factories****找 ApplicationContextInitializer

  • List<ApplicationContextInitializer<?>> initializers

  • ApplicationListener ;应用监听器。spring.factories****找 ApplicationListener

  • List<ApplicationListener<?>> listeners

  • 运行 SpringApplication

  • StopWatch

  • 记录应用的启动时间

  • **创建引导上下文(Context环境)**createBootstrapContext()

  • 获取到所有之前的 bootstrappers 挨个执行 intitialize() 来完成对引导启动器上下文环境设置

  • 让当前应用进入headless模式。java.awt.headless

  • 获取所有 RunListener**(运行监听器)【为了方便所有Listener进行事件感知】**

  • getSpringFactoriesInstances 去spring.factories****找 SpringApplicationRunListener.

  • 遍历 SpringApplicationRunListener 调用 starting 方法;

  • 相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting。

  • 保存命令行参数;ApplicationArguments

  • 准备环境 prepareEnvironment();

  • 返回或者创建基础环境信息对象。StandardServletEnvironment

  • 配置环境信息对象。

  • 读取所有的配置源的配置属性值。

  • 绑定环境信息

  • 监听器调用 listener.environmentPrepared();通知所有的监听器当前环境准备完成

  • 创建IOC容器(createApplicationContext())

  • 根据项目类型(Servlet)创建容器,

  • 当前会创建 AnnotationConfigServletWebServerApplicationContext

  • 准备ApplicationContext IOC容器的基本信息 prepareContext()

  • 保存环境信息

  • IOC容器的后置处理流程。

  • 应用初始化器;applyInitializers;

  • 遍历所有的 ApplicationContextInitializer 。调用 initialize.。来对ioc容器进行初始化扩展功能

  • 遍历所有的 listener 调用 contextPrepared。EventPublishRunListenr;通知所有的监听器****contextPrepared

  • 所有的监听器 调用 contextLoaded。通知所有的监听器 contextLoaded;

  • **刷新IOC容器。**refreshContext

  • 创建容器中的所有组件(Spring注解)

  • 容器刷新完成后工作?afterRefresh

  • 所有监听 器 调用 listeners.started(context); 通知所有的监听器 started

  • **调用所有runners;**callRunners()

  • 获取容器中的 ApplicationRunner

  • 获取容器中的 CommandLineRunner

  • 合并所有runner并且按照@Order进行排序

  • 遍历所有的runner。调用 run 方法

  • 如果以上有异常,

  • 调用Listener 的 failed

  • 调用所有监听器的 running 方法 listeners.running(context); 通知所有的监听器 running

  • **running如果有问题。继续通知 failed 。**调用所有 Listener 的 **failed;**通知所有的监听器 failed

public interface Bootstrapper {

	/**
	 * Initialize the given {@link BootstrapRegistry} with any required registrations.
	 * @param registry the registry to initialize
	 */
	void intitialize(BootstrapRegistry registry);

}
@FunctionalInterface
public interface ApplicationRunner {

	/**
	 * Callback used to run the bean.
	 * @param args incoming application arguments
	 * @throws Exception on error
	 */
	void run(ApplicationArguments args) throws Exception;

}
@FunctionalInterface
public interface CommandLineRunner {

	/**
	 * Callback used to run the bean.
	 * @param args incoming main method arguments
	 * @throws Exception on error
	 */
	void run(String... args) throws Exception;

}

2、Application Events and Listeners

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-application-events-and-listeners

ApplicationContextInitializer

ApplicationListener

SpringApplicationRunListener

3、ApplicationRunner 与 CommandLineRunner

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-03 11:01:36  更:2021-08-03 11:04:16 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/9 0:35:26-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码