Spring-Boot

SpringBoot2

一.SpringBoot2简介

1.学习要求

学习要求
-熟悉Spring基础
-熟悉Maven使用

环境要求
Java8及以上
Maven 3.3及以上

学习资料
Spring Boot官网
Spring Boot官方文档

2.Spring生态圈

Spring能做什么? Spring的能力?官网http://spring.io

Spring的生态覆盖了:

web开发
数据访问
安全控制
分布式
消息服务
移动开发
批处理

Spring5重大升级:

响应式编程

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

内部源码设计
基于Java8的一些新特性,如:接口默认实现。重新设计源码架构。

3.SpringBoot优点

为什么用SpringBoot?
Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”能快速创建出生产级别的Spring应用。

SpringBoot优点:
Create stand-alone Spring applications

创建独立Spring应用
Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)

内嵌web服务器
Provide opinionated ‘starter’ dependencies to simplify your build configuration

自动starter依赖,简化构建配置
Automatically configure Spring and 3rd party libraries whenever possible

自动配置Spring以及第三方功能
Provide production-ready features such as metrics, health checks, and externalized configuration

提供生产级别的监控、健康检查及外部化配置
Absolutely no code generation and no requirement for XML configuration

无代码生成、无需编写XML
SpringBoot是整合Spring技术栈的一站式框架

SpringBoot是简化Spring技术栈的快速开发脚手架

SpringBoot缺点:

人称版本帝,迭代快,需要时刻关注变化
封装太深,内部原理复杂,不容易精通

4.SpringBoot的大时代背景

微服务

In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.——James Lewis and Martin Fowler (2014)

微服务是一种架构风格,一个应用拆分为一组小型服务,每个服务运行在自己的进程内,也就是可独立部署和升级。服务之间使用轻量级HTTP交互,服务围绕业务功能拆分可以由全自动部署机制独立部署。去中心化,服务自治。服务可以使用不同的语言、不同的存储技术。

分布式

分布式的困难:
远程调用
服务发现
负载均衡
服务容错
配置管理
服务监控
链路追踪
日志管理
任务调度

分布式的解决:
SpringBoot + SpringCloud

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

云原生

原生应用如何上云。

上云的困难
服务自愈
弹性伸缩
服务隔离
自动化部署
灰度发布
流量治理

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

5.SpringBoot官方文档架构

Spring Boot官网https://spring.io/projects/spring-boot
Spring Boot官方文档https://docs.spring.io/spring-boot/docs/

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

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

查看版本新特性https://github.com/spring-projects/spring-boot/wiki#release-notes

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

二.SpringBoot初识

1.SpringBoot-HelloWorld

1.1 Maven配置

Maven配置文件settings.xml文件

<localRepository>E:/93/6.JavaWebProject/repolocalRepository>

<mirrors>
	<mirror>
		<id>nexus-aliyunid>
		<mirrorOf>centralmirrorOf>
		<name>Nexus aliyunname>
		<url>http://maven.aliyun.com/nexus/content/groups/publicurl>
	mirror>
mirrors>

<profiles>
	<profile>
		<id>jdk-1.8id>

		<activation>
			<activeByDefault>trueactiveByDefault>
			<jdk>1.8jdk>
		activation>

		<properties>
			<maven.compiler.source>1.8maven.compiler.source>
			<maven.compiler.target>1.8maven.compiler.target>
			<maven.compiler.compilerVersion>1.8maven.compiler.compilerVersion>
		properties>
	profile>
profiles>

1.2 新建Maven工程

需求:浏览发送/hello请求,响应 “Hello,Spring Boot 2”

第一步:创建maven工程

修改pom.xml,引入依赖


<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.4.4version>
parent>

<dependencies>
    
	<dependency>
		<groupId>org.springframework.bootgroupId>
		<artifactId>spring-boot-starter-webartifactId>
	dependency>
dependencies>

第二步:创建主程序

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @SpringBootApplication  主配置类
 * 1.识别到当前是一个SpringBoot的项目
 * 2.默认只会扫描同包下及其子包下的所有组件
 * 3.替代了以下三个注解
 * @SpringBootConfiguration
 * @EnableAutoConfiguration
 * @ComponentScan
 *
 * 4.通过指定scanBasePackages扫描包,让其包下的组件生效
 * @SpringBootApplication(scanBasePackages = "com.igeek.springboot")
 */
@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        /**
         * just run 直接运行即可
         * 第一个参数:运行哪一个SpringBoot的应用(主程序类)
         * 第二个参数:main方法的形参
         */
        //返回的就是IOC容器对象
        ConfigurableApplicationContext ac = SpringApplication.run(MainApplication.class, args);

        //查看IOC容器里面的组件,有哪些BeanDefinition
        String[] definitionNames = ac.getBeanDefinitionNames();
        for (String name : definitionNames) {
            System.out.println(name);
        }
    }
}

第三步:编写业务

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

/**
 * @RestController
 * 1.组合注解,替代下方的两个注解,当前类是Controller类且配置到IOC容器中
 * 2.当前Controller中的所有方法都是以json数据格式返回
 *
 * @Controller
 * @ResponseBody
 */
@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String handle01(){
        return "Hello, Spring Boot 2!";
    }
}

第四步:运行&测试

运行MainApplication类
浏览器输入http://localhost:8888/hello,页面显示Hello, Spring Boot 2!。

第五步:设置配置

maven工程的resources文件夹中创建application.properties文件。

#修改默认的端口号
server.port=8888

更多配置信息https://docs.spring.io/spring-boot/docs/2.3.7.RELEASE/reference/html/appendix-application-properties.html#common-application-properties-server

第六步:打包部署

在pom.xml添加

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

在IDEA的Maven插件上点击运行 clean 、package,把helloworld工程项目的打包成jar包,打包好的jar包被生成在helloworld工程项目的target文件夹内。

用cmd运行java -jar testSpringBoot-ch02-first-1.0-SNAPSHOT.jar,既可以运行helloworld工程项目。

将jar包直接在目标服务器执行即可。

解压jar包,可以发现内置许多jar包,例如内置的tomcat:

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

2.SpringBoot特性

2.1 SpringBoot-依赖管理特性

依赖管理

父项目做依赖管理:


<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.4.4version>
parent>

上面项目的父项目如下:


<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-dependenciesartifactId>
    <version>2.4.4version>
parent>

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

场景启动器

​ 开发导入starter场景启动器,见到很多 spring-boot-starter-* : *就某种场景只要引入starter,这个场景的所有常规需要的依赖我们都自动引入。更多SpringBoot所有支持的场景见到的 *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。

引入web启动器:


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

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


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starterartifactId>
    <version>2.4.4version>
    <scope>compilescope>
dependency>

自动版本仲裁

​ 无需关注版本号,自动版本仲裁。引入依赖默认都可以不写版本;引入非版本仲裁的jar,要写版本号,也可以修改默认版本号。

查看spring-boot-dependencies里面规定当前依赖的版本用的 key。
在当前项目里面重写配置,如下面的代码。


<properties>
    <mysql.version>5.1.38mysql.version>
properties>



<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    
    <version>${mysql.version}version>
dependency>

IDEA快捷键:

ctrl + shift + alt + U:以图的方式显示项目中依赖之间的关系。
alt + ins:相当于Eclipse的 Ctrl + N,创建新类,新包等。

2.2 SpringBoot-自动配置特性

自动配好Tomcat

引入Tomcat依赖。
配置Tomcat


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-tomcatartifactId>
    <version>2.4.4version>
    <scope>compilescope>
dependency>

自动配好SpringMVC

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

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

/**
 * @SpringBootApplication  主配置类
 * 1.识别到当前是一个SpringBoot的项目
 * 2.默认只会扫描同包下及其子包下的所有组件
 * 3.替代了以下三个注解
 * @SpringBootConfiguration
 * @EnableAutoConfiguration
 * @ComponentScan
 *
 * 4.通过指定scanBasePackages扫描包,让其包下的组件生效
 * @SpringBootApplication(scanBasePackages = "com.igeek.springboot")
 */
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {

        /**
         * just run 直接运行即可
         * 第一个参数:运行哪一个SpringBoot的应用(主程序类)
         * 第二个参数:main方法的形参
         */
        //1.返回的就是IOC容器对象
        ConfigurableApplicationContext ac = SpringApplication.run(MainApplication.class, args);

        //2.查看IOC容器里面的组件,有哪些BeanDefinition
        String[] definitionNames = ac.getBeanDefinitionNames();
        for (String name : definitionNames) {
            System.out.println(name);
        }
    }
}

默认的包结构

主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来,无需以前的包扫描配置。

想要改变扫描路径

@SpringBootApplication(scanBasePackages="com.igeek.springboot")

等同于
@ComponentScan("com.igeek.springboot")
@SpringBootApplication

等同于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.igeek.springboot")

各种配置拥有默认值

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

非常多的starter

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

3.SpringBoot底层注解

3.1 底层注解-@Configuration详解

1.配置类

package com.igeek.springboot.config;

import com.igeek.springboot.entity.Car;
import com.igeek.springboot.entity.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Configuration
 * 1.自定义的配置类,相当于xml配置文件,配置类本身也是组件
 * 2.配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的
 *
 * 3.属性proxyBeanMethods
 * 3.1 Full模式:默认是true
 *      > 用来监测当前IOC容器中是否有此实例,若有则不会再创建,直接返回此单实例;
 *      > 当前配置类的实例信息:com.igeek.springboot.config.MyConfig1$$EnhancerBySpringCGLIB$$7c35a8af@aa21042 代理对象
 * 3.2 Lite模式:指定为false
 *      > 每一次调用都会创建新的实例;
 *      > 当前配置类的实例信息:com.igeek.springboot.config.MyConfig1@45673f68
 *
 * 使用场景总结:
 * Lite模式,指定proxyBeanMethods值为false,配置类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断配置
 * Full模式,默认指定proxyBeanMethods值为true,类组件之间有依赖关系,方法会被调用得到之前单实例组件
 */
@Configuration(proxyBeanMethods = false)
public class MyConfig1 {

    /**
     * @Bean 注解
     * 1.将当前方法的返回值,添加至IOC容器中;
     * 2.默认使用方法名作为beanName
     * @return
     */
    @Bean("p1")
    public Person p(){
        Person p = new Person("李四","安徽淮北");
        //装配上Car
        p.setCar(car());
        return p;
    }

    @Bean
    public Car car(){
        return new Car("大奔",500000.0);
    }

}

2.测试类

@Configuration测试代码如下:

/**
 * @SpringBootApplication  主配置类
 * 1.识别到当前是一个SpringBoot的项目
 * 2.默认只会扫描同包下及其子包下的所有组件
 * 3.替代了以下三个注解
 * @SpringBootConfiguration
 * @EnableAutoConfiguration
 * @ComponentScan
 *
 * 4.通过指定scanBasePackages扫描包,让其包下的组件生效
 * @SpringBootApplication(scanBasePackages = "com.igeek.springboot")
 */
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {

        /**
         * just run 直接运行即可
         * 第一个参数:运行哪一个SpringBoot的应用(主程序类)
         * 第二个参数:main方法的形参
         */
        //1.返回的就是IOC容器对象
        ConfigurableApplicationContext ac = SpringApplication.run(MainApplication.class, args);

        //2.查看当前IOC容器中,有哪些bean实例
        Person p = ac.getBean("p1", Person.class);
        System.out.println("person:"+p);

        Car car1 = ac.getBean("car", Car.class);
        System.out.println("car1:"+car1);

        //3.获取配置类
        MyConfig1 myConfig1 = ac.getBean(MyConfig1.class);
        System.out.println("MyConfig1:"+myConfig1);
    }
}

3.使用场景

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

类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式(默认)

IDEA快捷键:
Alt + Ins:生成getter,setter、构造器等代码。

Ctrl + Alt + B:查看类的具体实现代码。

3.2 底层注解-@Import导入组件

1.常见注解

1.@Bean、@Component、@Controller、@Service、@Repository,它们是Spring的基本标签,在Spring Boot中并未改变它们原来的功能。

2.@ComponentScan添加指定容器扫描包

3.@Import({User.class})给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名

2.@Import注解

package com.igeek.springboot.config;

import com.igeek.springboot.entity.Person;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;

/**
 * @Configuration
 * 1.自定义的配置类,相当于xml配置文件
 * 2.从Spring的IOC容器中获取实例,默认是单例
 *
 * 将实例注册到IOC容器中:
 * 1.第一种方式:在方法上使用@Bean,将方法的返回值注册至IOC容器中,默认使用方法名作为beanName,但也可以指定,例如:p1
 
 * 2.第二种方式:通过在配置类@Configuration中,使用@Import({Person.class}),将实例注册到IOC容器中,默认使用实例的全类名作为beanName,例如:com.igeek.springboot.entity.Person
 
 * 3.第三种方式:通过在配置类@Configuration中,使用@ImportResource({"classpath:beans.xml"}),将xml配置文件导入,让其上配置的bean生效,例如beans.xml中配置的person的bean实例
 */
@Import({Person.class})
@ImportResource({"classpath:beans.xml"})
@Configuration
public class MyConfig2 {

}

3.测试类

package com.igeek.springboot;

import com.igeek.springboot.config.MyConfig1;
import com.igeek.springboot.config.MyConfig3;
import com.igeek.springboot.entity.Address;
import com.igeek.springboot.entity.Car;
import com.igeek.springboot.entity.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

/**
 * @SpringBootApplication  主配置类
 * 1.识别到当前是一个SpringBoot的项目
 * 2.默认只会扫描同包下及其子包下的所有组件
 * 3.替代了以下三个注解
 * @SpringBootConfiguration
 * @EnableAutoConfiguration
 * @ComponentScan
 *
 * 4.通过指定scanBasePackages扫描包,让其包下的组件生效
 * @SpringBootApplication(scanBasePackages = "com.igeek.springboot")
 */
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {

        /**
         * just run 直接运行即可
         * 第一个参数:运行哪一个SpringBoot的应用(主程序类)
         * 第二个参数:main方法的形参
         */
        //1.返回的就是IOC容器对象
        ConfigurableApplicationContext ac = SpringApplication.run(MainApplication.class, args);

        //2.获取当前IOC容器中的所有bean实例
        String[] beanNames = ac.getBeanNamesForType(Person.class);
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }

    }

}

3.3 底层注解-@Conditional条件装配

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

1.配置类

用@ConditionalOnMissingBean举例说明

package com.igeek.springboot.config;

import com.igeek.springboot.entity.Address;
import com.igeek.springboot.entity.Car;
import com.igeek.springboot.entity.Person;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;

/**
 * @Configuration
 * 1.自定义的配置类,相当于xml配置文件
 * 2.从Spring的IOC容器中获取实例,默认是单例
 *
 * @Conditional 条件装配
 * @ConditionalOnMissingBean 一旦IOC容器中没有指定的bean实例,则将当前的bean加入到IOC容器中
 * @ConditionalOnBean  一旦IOC容器中有指定的bean实例,则将当前的bean加入到IOC容器中
 */
@Configuration
//@ConditionalOnMissingBean(value = Car.class)
@ConditionalOnBean(value = Car.class)
public class MyConfig3 {

    //一旦IOC容器中没有c2的bean实例,就会装配上p2
    //@ConditionalOnMissingBean(value = Car.class)
    @Bean("p2")
    public Person p(){
        return new Person("李四","安徽淮北");
    }

    @Bean("c2")
    public Car car(){
        return new Car("大奔",500000.0);
    }

}

2.测试类

package com.igeek.springboot;

import com.igeek.springboot.config.MyConfig1;
import com.igeek.springboot.config.MyConfig3;
import com.igeek.springboot.entity.Address;
import com.igeek.springboot.entity.Car;
import com.igeek.springboot.entity.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

/**
 * @SpringBootApplication  主配置类
 * 1.识别到当前是一个SpringBoot的项目
 * 2.默认只会扫描同包下及其子包下的所有组件
 * 3.替代了以下三个注解
 * @SpringBootConfiguration
 * @EnableAutoConfiguration
 * @ComponentScan
 *
 * 4.通过指定scanBasePackages扫描包,让其包下的组件生效
 * @SpringBootApplication(scanBasePackages = "com.igeek.springboot")
 */
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {

        /**
         * just run 直接运行即可
         * 第一个参数:运行哪一个SpringBoot的应用(主程序类)
         * 第二个参数:main方法的形参
         */
        //1.返回的就是IOC容器对象
        ConfigurableApplicationContext ac = SpringApplication.run(MainApplication.class, args);

        //2.获取当前IOC容器中的所有bean实例
        String[] beanNames = ac.getBeanNamesForType(Person.class);
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }

        //3.获取配置类  NoSuchBeanDefinitionException
        MyConfig3 myConfig3 = ac.getBean(MyConfig3.class);
        System.out.println("MyConfig3:"+myConfig3);
    }

}

3.4 底层注解-@ImportResource导入Spring配置文件

假设原本的项目中仍然是SSM的,可以将所有的xml配置文件,通过@ImportResource导入进来使用

1.beans.xml


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="person" class="com.igeek.springboot.entity.Person" p:username="张三" p:address="江苏无锡" p:car-ref="car">bean>

    <bean id="car123" class="com.igeek.springboot.entity.Car" p:label="奥迪" p:price="300000.0">bean>

beans>

2.配置类

package com.igeek.springboot.config;

import com.igeek.springboot.entity.Person;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;

/**
 * @Configuration
 * 1.自定义的配置类,相当于xml配置文件
 * 2.从Spring的IOC容器中获取实例,默认是单例
 *
 * 将实例注册到IOC容器中:
 * 1.第一种方式:在方法上使用@Bean,将方法的返回值注册至IOC容器中,默认使用方法名作为beanName,但也可以指定,例如:p1
 * 2.第二种方式:通过在配置类@Configuration中,使用@Import({Person.class}),
 * 将实例注册到IOC容器中,默认使用实例的全类名作为beanName,例如:com.igeek.springboot.entity.Person
 * 3.第三种方式:通过在配置类@Configuration中,使用@ImportResource({"classpath:beans.xml"}),
 * 将xml配置文件导入,让其上配置的bean生效,例如beans.xml中配置的person的bean实例
 */
@Import({Person.class})
@ImportResource({"classpath:beans.xml"})
@Configuration
public class MyConfig2 {

}

3.测试类

public static void main(String[] args) {
    //1、返回我们IOC容器
    ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
    
    //2、测试是否包含指定的bean
    boolean person = run.containsBean("person");
    boolean car123 = run.containsBean("car123");
    System.out.println("person:"+person);//true
    System.out.println("car123:"+car123);//true
}

3.5 底层注解-@ConfigurationProperties配置绑定

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

1.传统方法

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

2.Spring Boot一种配置配置绑定

@ConfigurationProperties + @Component

2.1 假设有配置文件application.properties
myaddr.city=江苏无锡
myaddr.street=新吴区
2.2 pom.xml中

<dependency>
    <groupId>org.projectlombokgroupId>
    <artifactId>lombokartifactId>
dependency>

在File -> Settings ->Plugins -> 安装lombok插件

2.3 配置绑定
package com.igeek.springboot.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 配置绑定:
 * 1.@ConfigurationProperties(prefix = "myaddr")
 * 2.能够在properties文件上,使用指定的前缀操作类中的属性
 * 3.必须搭配使用,只有在容器中的组件,才会拥有SpringBoot提供的强大功能 
 
 * 第一种搭配使用:
 *      1).@Component + @ConfigurationProperties(prefix = "myaddr")
 *      2).只要在本实例bean中完成上述注解配置即可
 
 * 第二种搭配使用:
 *      1).@EnableConfigurationProperties(Address.class) + @ConfigurationProperties(prefix = "myaddr")
 *      2).@EnableConfigurationProperties在配置类中完成
 */
@Component
@ConfigurationProperties(prefix = "myaddr")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address {

    private String city;
    private String street;

}
2.4 测试类
package com.igeek.springboot;

import com.igeek.springboot.config.MyConfig1;
import com.igeek.springboot.config.MyConfig3;
import com.igeek.springboot.entity.Address;
import com.igeek.springboot.entity.Car;
import com.igeek.springboot.entity.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

/**
 * @SpringBootApplication  主配置类
 * 1.识别到当前是一个SpringBoot的项目
 * 2.默认只会扫描同包下及其子包下的所有组件
 * 3.替代了以下三个注解
 * @SpringBootConfiguration
 * @EnableAutoConfiguration
 * @ComponentScan
 *
 * 4.通过指定scanBasePackages扫描包,让其包下的组件生效
 * @SpringBootApplication(scanBasePackages = "com.igeek.springboot")
 */
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {

        /**
         * just run 直接运行即可
         * 第一个参数:运行哪一个SpringBoot的应用(主程序类)
         * 第二个参数:main方法的形参
         */
        //返回的就是IOC容器对象
        ConfigurableApplicationContext ac = SpringApplication.run(MainApplication.class, args);
        //获取Address实例
        Address address = ac.getBean(Address.class);

        //获取Address的bean实例
        System.out.println(address.getCity());
        System.out.println(address.getStreet());
    }

}

3.Spring Boot另一种配置配置绑定

@EnableConfigurationProperties + @ConfigurationProperties

3.1 假设有配置文件application.properties
myaddr.city=江苏无锡
myaddr.street=新吴区
3.2 配置类
package com.igeek.springboot.config;

import com.igeek.springboot.entity.Address;
import com.igeek.springboot.entity.Car;
import com.igeek.springboot.entity.Person;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;

/**
 * @Configuration
 * 1.自定义的配置类,相当于xml配置文件
 * 2.从Spring的IOC容器中获取实例,默认是单例
 */
@Configuration
//开启Address配置绑定功能把这个Address这个组件自动注册到容器中 
@EnableConfigurationProperties(Address.class)
public class MyConfig3 {
	
    
}
3.3 配置绑定
package com.igeek.springboot.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 配置绑定:
 * 1.@ConfigurationProperties(prefix = "myaddr")
 * 2.能够在properties文件上,使用指定的前缀操作类中的属性
 * 3.必须搭配使用,只有在容器中的组件,才会拥有SpringBoot提供的强大功能 
 
 * 第一种搭配使用:
 *      1).@Component + @ConfigurationProperties(prefix = "myaddr")
 *      2).只要在本实例bean中完成上述注解配置即可
 
 * 第二种搭配使用:
 *      1).@EnableConfigurationProperties(Address.class) + @ConfigurationProperties(prefix = "myaddr")
 *      2).@EnableConfigurationProperties在配置类中完成
 */
//@Component
@ConfigurationProperties(prefix = "myaddr")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address {

    private String city;
    private String street;

}
3.4 测试类
package com.igeek.springboot;

import com.igeek.springboot.config.MyConfig1;
import com.igeek.springboot.config.MyConfig3;
import com.igeek.springboot.entity.Address;
import com.igeek.springboot.entity.Car;
import com.igeek.springboot.entity.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

/**
 * @SpringBootApplication  主配置类
 * 1.识别到当前是一个SpringBoot的项目
 * 2.默认只会扫描同包下及其子包下的所有组件
 * 3.替代了以下三个注解
 * @SpringBootConfiguration
 * @EnableAutoConfiguration
 * @ComponentScan
 *
 * 4.通过指定scanBasePackages扫描包,让其包下的组件生效
 * @SpringBootApplication(scanBasePackages = "com.igeek.springboot")
 */
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
        /**
         * just run 直接运行即可
         * 第一个参数:运行哪一个SpringBoot的应用(主程序类)
         * 第二个参数:main方法的形参
         */
        //返回的就是IOC容器对象
        ConfigurableApplicationContext ac = SpringApplication.run(MainApplication.class, args);
        //获取Address实例
        Address address = ac.getBean(Address.class);

        //获取Address的bean实例
        System.out.println(address.getCity());
        System.out.println(address.getStreet());
    }

}

4.自动配置【源码分析】

4.1 自动配置【源码分析】- 自动包规则原理

Spring Boot应用的启动类

@SpringBootApplication
public class MainApplication {

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

}

1)、分析@SpringBootApplication

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    ...
}

注意:重点分析下@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan。

2)、分析@SpringBootConfiguration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

注意:@Configuration代表当前是一个配置类。

3)、分析@ComponentScan

指定包,扫描哪些Spring注解。

4)、分析@EnableAutoConfiguration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};
    
    String[] excludeName() default {};

}

注意:重点分析@AutoConfigurationPackage,@Import(AutoConfigurationImportSelector.class)

5)、分析@AutoConfigurationPackage

标签名直译为:自动配置包,指定了默认的包规则。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)//给容器中导入一个组件
public @interface AutoConfigurationPackage {
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

}

利用Registrar给容器中导入一系列组件
将指定的一个包下的所有组件导入进MainApplication所在包下。

4.2 自动配置【源码分析】- 初始加载自动配置类

1)、启动时默认全场景加载

@Import(AutoConfigurationImportSelector.class)

1.利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件

2.调用List configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类

3.利用工厂加载 Map loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件

4.从META-INF/spring.factories位置来加载一个文件。
1.默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
2.spring-boot-autoconfigure-2.4.4.RELEASE.jar包里面也有META-INF/spring.factories

130个场景的所有自动配置,在启动的时候默认全部加载

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

#文件里面写了spring-boot一启动就要给容器中加载的所有配置类
#spring-boot-autoconfigure-2.4.4.RELEASE.jar/META-INF/spring.factories
#Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
...

2)、按照条件装配规则按需配置

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

如AopAutoConfiguration类,@ConditionalOnClass(Advice.class),未导入org.aspectj.weaver.Advice依赖,则AspectJAutoProxyingConfiguration配置就不会生效。

如DispatcherServletAutoConfiguration类,@ConditionalOnClass(DispatcherServlet.class),导入了spring-webmvc的依赖,则DispatcherServletConfiguration配置就会生效。

4.3 自动配置【源码分析】- 自动配置流程

1)、以DispatcherServletAutoConfiguration为例

以DispatcherServletAutoConfiguration的内部类DispatcherServletConfiguration为例:

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

2)、自定义配置优先

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

以HttpEncodingAutoConfiguration为例:

@Bean
//若IOC容器中没有CharacterEncodingFilter组件(自定义配置类中的组件),则生效
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
    CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
    filter.setEncoding(this.properties.getCharset().name());
    filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
    filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
    return filter;
}

3)、总结

第一步:SpringBoot先默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件,加载所有的自动配置类XxxAutoConfiguration

第二步:每个自动配置类XxxAutoConfiguration,会按照条件@Conditional生效,按需配置

第三步:生效的配置类XxxAutoConfiguration中,会通过@Bean给容器中装配很多组件,只要容器中有这些组件,就会具备对应的功能

第四步:配置类XxxAutoConfiguration中,都会有@EnableConfigurationProperties(XxxProperties.class),来进行配置绑定(默认都会绑定配置文件中指定的值)

第五步:XxxProperties类中,会提供操作本类中属性的前缀 @ConfigurationProperties(prefix = “server”)

第六步:若要定制化配置,则:

​ 1.通过@Configuration自定义配置类,通过@Bean替换底层的组件;

​ 2.通过查看此组件上@EnableConfigurationProperties(XxxProperties.class)指定的配置类,在properties上或yaml文件上进行修改其属性;

​ 3.自定义定制器 XxxCustomizer;(后续讲)

XxxAutoConfiguration —> @Bean组件 —> XxxProperties拿值 ----> application.properties

5.SpringBoot应用如何创建

第一步:引入场景依赖

官方文档

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

第二步:查看哪些进行自动配置

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

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

第三步:是否需要修改

1.参照文档修改配置项
官方文档https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties

​ 自己分析。xxxxProperties绑定了配置文件的哪些配置。

2.自定义加入或者替换组件
@Bean、@Component…

3.自定义器 XXXXXCustomizer

6.开发技巧

6.1 Lombok简化开发

Lombok用标签方式代替构造器、getter/setter、toString()等鸡肋代码。

spring boot已经管理Lombok。

引入依赖:

<dependency>
    <groupId>org.projectlombokgroupId>
    <artifactId>lombokartifactId>
dependency>

IDEA中File->Settings->Plugins,搜索安装Lombok插件

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

示例一:实体类

@NoArgsConstructor
//@AllArgsConstructor
@Data
@ToString
@EqualsAndHashCode
public class User {

    private String name;
    private Integer age;
    
    private Pet pet;
    
    public User(String name,Integer age){
        this.name = name;
        this.age = age;
    }

}

示例二:简化日志开发

@Slf4j
@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String hello(@RequestParam("name") String name){
        log.info("hello请求....");
        return "Hello, Spring Boot 2!"+"你好:"+name;
    }
}

6.2 dev-tools

Spring Boot includes an additional set of tools that can make the application development experience a little more pleasant. The spring-boot-devtools module can be included in any project to provide additional development-time features.——link(https://docs.spring.io/spring-boot/docs/2.3.8.RELEASE/reference/html/using-spring-boot.html#using-boot-devtools)

Applications that use spring-boot-devtools automatically restart whenever files on the classpath change. This can be a useful feature when working in an IDE, as it gives a very fast feedback loop for code changes. By default, any entry on the classpath that points to a directory is monitored for changes. Note that certain resources, such as static assets and view templates, do not need to restart the application.——link(https://docs.spring.io/spring-boot/docs/2.3.8.RELEASE/reference/html/using-spring-boot.html#using-boot-devtools-restart)

Triggering a restart

As DevTools monitors classpath resources, the only way to trigger a restart is to update the classpath. The way in which you cause the classpath to be updated depends on the IDE that you are using:

In Eclipse, saving a modified file causes the classpath to be updated and triggers a restart.
In IntelliJ IDEA, building the project (Build -> Build Project)(shortcut: Ctrl+F9) has the same effect.

添加依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-devtoolsartifactId>
        <optional>trueoptional>
    dependency>
dependencies>

在IDEA中,项目或者页面修改以后:Ctrl+F9。

6.3 Spring Initailizr

Spring Initailizr(https://start.spring.io/)是创建Spring Boot工程向导。

在IDEA中,菜单栏New -> Project -> Spring Initailizr

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

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

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

项目结构:

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

三.配置文件

1.配置文件类型

1.1 properties文件

同以前的properties用法

@Data
@Component
@ConfigurationProperties(prefix = "person")
public class Person {

    private String name;
    private boolean gender;
    private Date birthday;
    private List<String> hobbys;

    private Car car;
    private Map<String,Car> carMap;
    private Map<String,List<Car>> mapList;
}
# 配置Person信息
person.name=张三
person.gender=true
person.birthday=2020/12/12
person.hobbys=[编程,音乐,篮球]

person.car.label=奥迪
person.car.price=200000.0

# Map
person.car-map.AAA.label=奔驰
person.car-map.AAA.price=500000.0

# Map>
person.map-list.BBB[0].label=沃尔沃
person.map-list.BBB[0].price=600000.0

person.map-list.BBB[1].label=比亚迪
person.map-list.BBB[1].price=500000.0
@SpringBootTest
class SpringbootCh03ConfigApplicationTests {

    @Autowired
    private Person person;

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

}

1.2 YAML的用法

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

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

1)、基本语法

key: value;kv之间有空格

大小写敏感

使用缩进表示层级关系

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

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

'#'表示注释

字符串无需加引号,如果要加,单引号’’、双引号""表示字符串内容会被 转义 / 不转义

2)、数据类型

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

k: v

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

#行内写法:  
k: {k1: v1,k2: v2,k3: v3}

#或
k: 
  k1: v1
  k2: v2
  k3: v3

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

#行内写法:  
k: [v1,v2,v3]

#或者
k:
  - v1
  - v2
  - v3

3)、实例

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


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

用yaml表示以上对象

person:
  userName: zhangsan
  boss: false
  birth: 2019/12/12 20:12:33
  age: 18
  pet: 
    name: tomcat
    weight: 23.4
  interests: [篮球,游泳]
  animal: 
    - jerry
    - mario
  score:
    english: 
      first: 30
      second: 40
      third: 50
    math: [131,140,148]
    chinese: {first: 128,second: 136}
  salarys: [3999,4999.98,5999.99]
  allPets:
    sick:
      - {name: tom}
      - {name: jerry,weight: 47}
    health: [{name: mario,weight: 47}]

2.自定义类绑定的配置提示

You can easily generate your own configuration metadata file from items annotated with @ConfigurationProperties by using the spring-boot-configuration-processor jar. The jar includes a Java annotation processor which is invoked as your project is compiled.——link(https://docs.spring.io/spring-boot/docs/2.4.2/reference/htmlsingle/#configuration-metadata-annotation-processor)

自定义的类和配置文件绑定一般没有提示。若要提示,添加如下依赖:


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-configuration-processorartifactId>
    <optional>trueoptional>
dependency>


<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-maven-pluginartifactId>
            
            <configuration>
                <excludes>
                    <exclude>
                        <groupId>org.springframework.bootgroupId>
                        <artifactId>spring-boot-configuration-processorartifactId>
                    exclude>
                excludes>
            configuration>
            
        plugin>
    plugins>
build>

3.课堂练习

public class Person {

    //字面量:  字符串  整型   时间   布尔
    private String name;
    private Integer age;
    private Date birthday;
    private Boolean flag;

    //对象、Map : 键值对
    private Car car;
    private Map<String,Object> maps;

    //数组(List、Set) : 一组值
    private List<String> list;
    private List<Object> listObj;

    //复合结构
    private List<Car> listCars;
    private List<Map<String,Object>> listMap;
    private Map<String,List<Object>> mapList;
    
}


public class Car {
    private String name;
    private Double price;
}

要求1:使用properties文件,完成自定义的类和配置文件绑定

要求2:使用yaml文件,完成自定义的类和配置文件绑定

四.Web开发

web开发简介

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

(大多场景我们都无需自定义配置)

The auto-configuration adds the following features on top of Spring’s defaults:

1.Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.

内容协商视图解析器和BeanName视图解析器

2.Support for serving static resources, including support for WebJars (covered later in this document)).

静态资源(包括webjars)

3.Automatic registration of Converter, GenericConverter, and Formatter beans.

自动注册 Converter,GenericConverter,Formatter

4.Support for HttpMessageConverters (covered later in this document).

支持 HttpMessageConverters

5.Automatic registration of MessageCodesResolver (covered later in this document).

自动注册 MessageCodesResolver (国际化用)

6.Static index.html support.

静态index.html 页支持

7.Custom Favicon support (covered later in this document).

自定义 Favicon

8.Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)

If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.

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

If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.

声明 WebMvcRegistrations 改变默认底层组件

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.

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

1.静态资源

1.1 静态资源规则与定制化

1)、静态资源目录

只要静态资源放在类路径下: /static (or /public or /resources or /META-INF/resources)

访问时路径 : 当前项目根路径/ + 静态资源名

**原理: 静态映射/****。

请求进来,先去找Controller看能不能处理。若Controller不能处理,则将请求又交给静态资源处理器。静态资源也找不到则响应404页面。

2)、静态资源访问前缀

#静态资源访问时默认无前缀,可以改变静态资源访问的前缀路径,访问时则:http://localhost:8899/res/资源名
spring:
  mvc:
    static-path-pattern: /res/**

  #修改静态资源默认的文件夹名,让/static,/public , /resources , /META-INF/resources失效
  web: 
	resources:
    	static-locations: [classpath:/abc/]

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

3)、webjars

自动映射 /webjars/**

可用jar方式添加css,js等资源文件,https://www.webjars.org/

例如,添加jquery的webjars资源依赖: 查看jar包,资源在 /META-INF/resources文件夹下

<dependency>
    <groupId>org.webjarsgroupId>
    <artifactId>jqueryartifactId>
    <version>3.5.1version>
dependency>

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

1.2 welcome与favicon功能

1)、欢迎页支持

方式一:静态资源路径下 index.html

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

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

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

方式二:controller能处理/index

2)、自定义Favicon

指网页标签上的小图标。

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

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

1.3 【源码分析】-静态资源规则原理

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

WebMvcAutoConfigurationAdapter给容器中配置的内容:

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

配置文件的相关属性的绑定:WebMvcProperties==spring.mvc、ResourceProperties==spring.resources

配置类只有一个有参构造器

有参构造器所有参数的值都会从容器中确定

//有参构造器所有参数的值都会从容器中确定
//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;
}

1)、资源处理的默认规则

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

    //默认的静态资源访问路径的规则
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    if (!registry.hasMappingForPattern(staticPathPattern)) {
      customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                                             .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
}

根据上述代码,我们可以同过配置禁止所有静态资源规则。

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

  web:
    resources:
      add-mappings: false  # 禁用默认的静态资源访问路径

静态资源规则:

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

2)、欢迎页的处理规则

//HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。	
@Bean
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
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
			ApplicationContext applicationContext, 
            Optional<Resource> welcomePage, String staticPathPattern) {
    if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
        //要用欢迎页功能,必须是/**
        logger.info("Adding welcome page: " + welcomePage.get());
        setRootViewName("forward:index.html");
    }
    else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
        // 调用Controller  /index
        logger.info("Adding welcome page template: index");
        setRootViewName("index");
    }
}

这构造方法内的代码也解释了web场景-welcome与favicon功能中配置static-path-pattern了,welcome页面和小图标失效的问题。

2.请求处理

2.1 Rest请求映射

1)、请求映射

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

以前:
/getUser 获取用户
/deleteUser 删除用户
/editUser 修改用户
/saveUser保存用户

现在: /user
GET-获取用户
DELETE-删除用户
PUT-修改用户
POST-保存用户

@XxxMapping; 替换 @RequestMapping(value = “/rest”,method= RequestMethod.XXX)

@GetMapping
@PostMapping
@PutMapping
@DeleteMapping

核心Filter:HiddenHttpMethodFilter 中doFilterInternal()执行过滤器逻辑

2)、用法

第一步:开启页面表单的Rest功能

页面 form的属性method=post,隐藏域 _method=put、delete等(如果直接get或post,无需隐藏域)

<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>
第二步:Controller请求映射
@GetMapping("/user")
//@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
    return "GET-张三";
}

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

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

@DeleteMapping("/user")
//@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
    return "DELETE-张三";
}
第三步:修改默认配置开启支持PUT、DELETE方式处理请求
# 开启页面表单的Rest功能(支持PUT、DELETE)
spring.mvc.hiddenmethod.filter.enabled=true

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

1.表单提交会带上 _method=PUT

2.请求过来被HiddenHttpMethodFilter拦截

3.请求是否正常,并且是POST

4.获取到_method的值

5.兼容以下请求:PUT.DELETE.PATCH

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

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

public class HiddenHttpMethodFilter extends OncePerRequestFilter {

    private static final List<String> ALLOWED_METHODS =
    		Collections.unmodifiableList(Arrays.asList(
                HttpMethod.PUT.name(),
    			HttpMethod.DELETE.name(), 
                HttpMethod.PATCH.name()));
    
    /** Default method parameter: {@code _method}. */
    public static final String DEFAULT_METHOD_PARAM = "_method";
    
    private String methodParam = DEFAULT_METHOD_PARAM;

    /**
     * Set the parameter name to look for HTTP methods.
     * @see #DEFAULT_METHOD_PARAM
     */
    public void setMethodParam(String methodParam) {
    Assert.hasText(methodParam, "'methodParam' must not be empty");
    	this.methodParam = methodParam;
    }
    
    @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);
    }

    /**
     * Simple {@link HttpServletRequest} wrapper that returns the supplied method for
     * {@link HttpServletRequest#getMethod()}.
     */
    private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
    
    	private final String method;
    
    	public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
    		super(request);
    		this.method = method;
    	}
    
    	@Override
    	public String getMethod() {
    		return this.method;
    	}
    }

}

4)、Rest使用客户端工具

如PostMan可直接发送put、delete等方式请求。

5)、怎么改变默认的_method

@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 {
    ...
    
    @Bean
    @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
    @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new OrderedHiddenHttpMethodFilter();
    }
    
    ...

}

**@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)**意味着在没有HiddenHttpMethodFilter时,才执行hiddenHttpMethodFilter()。因此,我们可以自定义filter,改变默认的_method。

自定义配置类,例如:

@Configuration(proxyBeanMethods = false)
public class WebConfig{
    
    //自定义filter
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
        methodFilter.setMethodParam("_m");
        return methodFilter;
    }    
    
}

将页面中的_method改成_m

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

2.2 【源码分析】-请求映射原理

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

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

            // 找到当前请求使用哪个Handler(Controller的方法)处理
            mappedHandler = getHandler(processedRequest);

            //HandlerMapping:处理器映射。/xxx->>xxxx
    ...
}

this.handlerMappings在Debug模式下展现的内容:

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

RequestMappingHandlerMapping: 其中,保存了所有@RequestMappinghandler的映射规则。

mapping --> mappingRegistry --> mappingLookup

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

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

2.SpringBoot自动配置了默认 的 RequestMappingHandlerMapping

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

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

​ 如果没有就是下一个 HandlerMapping

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

getHandler()方法如下:

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

IDEA快捷键:

  • Ctrl + Alt + U : 以UML的类图展现类有哪些继承类,派生类以及实现哪些接口。
  • Crtl + Alt + Shift + U : 同上,区别在于上条快捷键结果在新页展现,而本条快捷键结果在弹窗展现。
  • Ctrl + H : 以树形方式展现类层次结构图。

2.3 参数注解

注解:

@PathVariable 路径变量
@RequestHeader 获取请求头
@RequestParam 获取请求参数(指问号后的参数,url?a=1&b=2)
@CookieValue 获取Cookie值
@RequestAttribute 获取request域属性
@RequestBody 获取请求体[POST]
@MatrixVariable 矩阵变量
@ModelAttribute 请求域中变量

1)、@PathVariable 路径变量

@RestController
public class ParamController {

    /**
     *  1.@PathVariable 路径变量
     *  访问路径  http://localhost:8899/testPath/zs/123
     *
     *  请求路径中  "/testPath/{name}/{pwd}"  {}占位符,
     *  通过@PathVariable注解获得对应位置的请求参数值
	 *     
     *  @PathVariable Map pv 直接获取请求路径中,所有路径变量
     */
    @GetMapping("/testPath/{name}/{pwd}")
    @ResponseBody
    public Map<String,Object> testPathVariable(
            @PathVariable("name") String username,
            @PathVariable("pwd") String password,
            @PathVariable Map<String,String> pv){
        Map<String,Object> map = new HashMap<>();
        map.put("username",username);
        map.put("password",password);
        map.put("pv",pv);
        return map;
    }
}

Postman测试:

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

2)、@RequestParam 获取请求参数

/**
 * 3.@RequestParam 获取请求参数
 * 请求地址  /testParam/{id}?sex=0&pwd=123&pwd=456  进行访问
 * ?sex=0&pwd=123&pwd=456  交由@RequestParam处理请求参数
 *
 * @RequestParam MultiValueMap params  获得所有请求参数中的值
 */
@GetMapping("/testParam/{id}")
@ResponseBody
public Map<String,Object> testRequestParam(
    @PathVariable("id") Integer id,
    @RequestParam("sex") String sex,
    @RequestParam("pwd") List<String> pwd,
    @RequestParam MultiValueMap<String,String> params){
    
    Map<String,Object> map = new HashMap<>();
    map.put("id",id);
    map.put("sex",sex);
    map.put("pwd",pwd);
    map.put("params",params);
    return map;
}

Postman测试:

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

3)、@RequestAttribute 获取request域属性

/**
 * 5.@RequestAttribute 获取request域属性
 */
@GetMapping("/testAttr/{id}")
public String testRequestAttr(
    @PathVariable("id")Integer id ,
    HttpServletRequest request){
    //设置请求域中的属性值
    request.setAttribute("idNew",id);
    //请求转发,将当前请求延续下去,携带请求数据
    return "forward:/success";
}

@GetMapping("/success")
@ResponseBody
public Map<String,Object> testSuccess(
    @RequestAttribute("idNew") Integer id){
    Map<String,Object> map = new HashMap<>();
    map.put("idNew",id);
    return map;
}

测试:

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

4)、@RequestBody 获取请求体

/**
 * 6.@RequestBody 获取请求体
 */
@PostMapping("/add")
@ResponseBody
public Map<String,Object> add(@RequestBody String data){
    Map<String,Object> map = new HashMap<>();
    map.put("data",data);
    return map;
}

测试:

Postman测试

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

浏览器测试:

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

2.4 Servlet API参数

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

ServletRequestMethodArgumentResolver用来处理以上的参数

public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {

	@Nullable
	private static Class<?> pushBuilder;

	static {
		try {
			pushBuilder = ClassUtils.forName("javax.servlet.http.PushBuilder",
					ServletRequestMethodArgumentResolver.class.getClassLoader());
		}
		catch (ClassNotFoundException ex) {
			// Servlet 4.0 PushBuilder not found - not supported for injection
			pushBuilder = null;
		}
	}


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

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		Class<?> paramType = parameter.getParameterType();

		// WebRequest / NativeWebRequest / ServletWebRequest
		if (WebRequest.class.isAssignableFrom(paramType)) {
			if (!paramType.isInstance(webRequest)) {
				throw new IllegalStateException(
						"Current request is not of type [" + paramType.getName() + "]: " + webRequest);
			}
			return webRequest;
		}

		// ServletRequest / HttpServletRequest / MultipartRequest / MultipartHttpServletRequest
		if (ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType)) {
			return resolveNativeRequest(webRequest, paramType);
		}

		// HttpServletRequest required for all further argument types
		return resolveArgument(paramType, resolveNativeRequest(webRequest, HttpServletRequest.class));
	}

	private <T> T resolveNativeRequest(NativeWebRequest webRequest, Class<T> requiredType) {
		T nativeRequest = webRequest.getNativeRequest(requiredType);
		if (nativeRequest == null) {
			throw new IllegalStateException(
					"Current request is not of type [" + requiredType.getName() + "]: " + webRequest);
		}
		return nativeRequest;
	}

	@Nullable
	private Object resolveArgument(Class<?> paramType, HttpServletRequest request) throws IOException {
		if (HttpSession.class.isAssignableFrom(paramType)) {
			HttpSession session = request.getSession();
			if (session != null && !paramType.isInstance(session)) {
				throw new IllegalStateException(
						"Current session is not of type [" + paramType.getName() + "]: " + session);
			}
			return session;
		}
		else if (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) {
			return PushBuilderDelegate.resolvePushBuilder(request, paramType);
		}
		else if (InputStream.class.isAssignableFrom(paramType)) {
			InputStream inputStream = request.getInputStream();
			if (inputStream != null && !paramType.isInstance(inputStream)) {
				throw new IllegalStateException(
						"Request input stream is not of type [" + paramType.getName() + "]: " + inputStream);
			}
			return inputStream;
		}
		else if (Reader.class.isAssignableFrom(paramType)) {
			Reader reader = request.getReader();
			if (reader != null && !paramType.isInstance(reader)) {
				throw new IllegalStateException(
						"Request body reader is not of type [" + paramType.getName() + "]: " + reader);
			}
			return reader;
		}
		else if (Principal.class.isAssignableFrom(paramType)) {
			Principal userPrincipal = request.getUserPrincipal();
			if (userPrincipal != null && !paramType.isInstance(userPrincipal)) {
				throw new IllegalStateException(
						"Current user principal is not of type [" + paramType.getName() + "]: " + userPrincipal);
			}
			return userPrincipal;
		}
		else if (HttpMethod.class == paramType) {
			return HttpMethod.resolve(request.getMethod());
		}
		else if (Locale.class == paramType) {
			return RequestContextUtils.getLocale(request);
		}
		else if (TimeZone.class == paramType) {
			TimeZone timeZone = RequestContextUtils.getTimeZone(request);
			return (timeZone != null ? timeZone : TimeZone.getDefault());
		}
		else if (ZoneId.class == paramType) {
			TimeZone timeZone = RequestContextUtils.getTimeZone(request);
			return (timeZone != null ? timeZone.toZoneId() : ZoneId.systemDefault());
		}

		// Should never happen...
		throw new UnsupportedOperationException("Unknown parameter type: " + paramType.getName());
	}


	/**
	 * Inner class to avoid a hard dependency on Servlet API 4.0 at runtime.
	 */
	private static class PushBuilderDelegate {

		@Nullable
		public static Object resolvePushBuilder(HttpServletRequest request, Class<?> paramType) {
			PushBuilder pushBuilder = request.newPushBuilder();
			if (pushBuilder != null && !paramType.isInstance(pushBuilder)) {
				throw new IllegalStateException(
						"Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder);
			}
			return pushBuilder;

		}
	}
}

2.5 复杂参数

Map

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

RedirectAttributes( 重定向携带数据)

ServletResponse(response响应)

Errors/BindingResult

SessionStatus

UriComponentsBuilder

ServletUriComponentsBuilder

1)、Model、Map案例 (request请求域)

map、model里面的数据会被放在request的请求域 request.setAttribute

@Controller
public class ModelMapController {

    /**
     * 以下参数,都是将数据放至请求域中
     * @param map
     * @param model
     * @param modelMap
     * @param request
     * @return
     */
    @GetMapping(value = "/modelAdd")
    public String modelAdd(
            Map<String,String> map ,
            Model model,
            ModelMap modelMap,
            HttpServletRequest request){

        //以下都会将数据放至请求域中
        map.put("k1","map&v1");
        model.addAttribute("k2","model&v2");
        modelMap.addAttribute("k3","modelMap&v3");
        request.setAttribute("k4","request&v4");

        //保持原来的请求,请求域中的数据可以携带至success中
        return "redirect:/modelSuccess";
    }

    @GetMapping("/modelSuccess")
    @ResponseBody
    public Map<String,Object> modelSuccess(HttpServletRequest request){
        Map<String,Object> map = new HashMap<>();

        //从请求域中获取请求属性
        Object v1 = request.getAttribute("k1");
        Object v2 = request.getAttribute("k2");
        Object v3 = request.getAttribute("k3");
        Object v4 = request.getAttribute("k4");

        //添加至map集合中
        map.put("map",v1);
        map.put("model",v2);
        map.put("modelMap",v3);
        map.put("request",v4);

        return map;
    }
}

  • Map map
  • Model model
  • HttpServletRequest request

上面三位都是可以给request域中放数据,用request.getAttribute()获取

2)、ServletResponse案例(response响应)

@Controller
public class ModelMapController {

    /**
     * 以下参数,都是将数据放至请求域中
     * @param map
     * @param model
     * @param modelMap
     * @param request
     * @param response
     * @return
     */
    @GetMapping(value = "/modelAdd")
    public String modelAdd(
            Map<String,String> map ,
            Model model,
            ModelMap modelMap,
            HttpServletRequest request,
            HttpServletResponse response){

        //以下都会将数据放至请求域中
        map.put("k1","map&v1");
        model.addAttribute("k2","model&v2");
        modelMap.addAttribute("k3","modelMap&v3");
        request.setAttribute("k4","request&v4");

        //在响应中添加Cookie,在页面中可看到添加的cookie
        Cookie cookie = new Cookie("kaaa","vaaa");
        response.addCookie(cookie);

        //保持原来的请求,请求域中的数据可以携带至success中
        return "redirect:/modelSuccess";
    }

    @GetMapping("/modelSuccess")
    @ResponseBody
    public Map<String,Object> modelSuccess(HttpServletRequest request){
        Map<String,Object> map = new HashMap<>();

        //从请求域中获取请求属性
        Object v1 = request.getAttribute("k1");
        Object v2 = request.getAttribute("k2");
        Object v3 = request.getAttribute("k3");
        Object v4 = request.getAttribute("k4");

        //添加至map集合中
        map.put("map",v1);
        map.put("model",v2);
        map.put("modelMap",v3);
        map.put("request",v4);

        return map;
    }
}

3)、RedirectAttributes案例( 重定向携带数据)

@Controller
public class RedirectAttributesController {

    //重定向时,强制追加请求参数
    @RequestMapping("/addRedirectAttributes")
    public String addRedirectAttributes(RedirectAttributes redirectAttributes){
        //响应重定向时,强制追加的请求参数  http://localhost:8899/successRedirectAttributes?kk1=vv1&kk2=vv2
        redirectAttributes.addAttribute("kk1","vv1");
        redirectAttributes.addAttribute("kk2","vv2");
        return "redirect:/successRedirectAttributes";
    }


    @RequestMapping("/successRedirectAttributes")
    @ResponseBody
    public Map<String,Object> successRedirectAttributes(@RequestParam Map maps){
        Map<String,Object> map = new HashMap<>();
        map.put("maps",maps);
        return map;
    }

}

测试: http://localhost:8899/addRedirectAttributes

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

4)、【源码分析】

接下来我们看看,Map mapModel model用什么参数处理器。

Map map参数用MapMethodProcessor处理:

public class MapMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

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

	@Override
	@Nullable
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure");
		return mavContainer.getModel();
	}
    
    ...
    
}

mavContainer.getModel()如下:

public class ModelAndViewContainer {

    ...

	private final ModelMap defaultModel = new BindingAwareModelMap();

	@Nullable
	private ModelMap redirectModel;

    ...

	public ModelMap getModel() {
		if (useDefaultModel()) {
			return this.defaultModel;
		}
		else {
			if (this.redirectModel == null) {
				this.redirectModel = new ModelMap();
			}
			return this.redirectModel;
		}
	}
    
    private boolean useDefaultModel() {
		return (!this.redirectModelScenario || (this.redirectModel == null && !this.ignoreDefaultModelOnRedirect));
	}
    ...
    
}

Model modelModelMethodProcessor处理:

public class ModelMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return Model.class.isAssignableFrom(parameter.getParameterType());
	}

	@Override
	@Nullable
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure");
		return mavContainer.getModel();
	}
    ...
}

return mavContainer.getModel();这跟MapMethodProcessor的一致

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

Model也是另一种意义的Map

接下来看看Map map与Model model值是如何做到用request.getAttribute()获取的。

众所周知,所有的数据都放在 ModelAndView包含要去的页面地址View,还包含Model数据。

先看ModelAndView接下来是如何处理的?

public class DispatcherServlet extends FrameworkServlet {
    
    ...
    
	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		...

		try {
			ModelAndView mv = null;
            
            ...

			// Actually invoke the handler.
			mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
            
            ...
            
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
        	//处理分发结果
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
        ...

	}

	private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {
        ...

		// Did the handler return a view to render?
		if (mv != null && !mv.wasCleared()) {
			render(mv, request, response);
			...
		}
		...
	}

	protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
		...

		View view;
		String viewName = mv.getViewName();
		if (viewName != null) {
			// We need to resolve the view name.
			view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
			if (view == null) {
				throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
						"' in servlet with name '" + getServletName() + "'");
			}
		}
		else {
			// No need to lookup: the ModelAndView object contains the actual View object.
			view = mv.getView();
			if (view == null) {
				throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
						"View object in servlet with name '" + getServletName() + "'");
			}
		}
		view.render(mv.getModelInternal(), request, response);
        
        ...
	}

}

在Debug模式下,view属为InternalResourceView类。

public class InternalResourceView extends AbstractUrlBasedView {
    
 	@Override//该方法在AbstractView,AbstractUrlBasedView继承了AbstractView
	public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		
        ...
        
		Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
		prepareResponse(request, response);
        
        //看下一个方法实现
		renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
	}
    
    @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);
		
        ...
	}
    
    //该方法在AbstractView,AbstractUrlBasedView继承了AbstractView
    protected void exposeModelAsRequestAttributes(Map<String, Object> model,
			HttpServletRequest request) throws Exception {

		model.forEach((name, value) -> {
			if (value != null) {
				request.setAttribute(name, value);
			}
			else {
				request.removeAttribute(name);
			}
		});
	}
    
}

exposeModelAsRequestAttributes方法看出,Map mapModel model这两种类型数据可以给request域中放数据,用request.getAttribute()获取。

2.6 自定义参数绑定 封装POJO

1)、案例

index.html: 可以自动类型转换与格式化,可以级联封装。

<form action="saveUser" method="post">
    姓名: <input name="userName"/><br/>
    年龄: <input name="age"/><br/>
    生日: <input name="birth"/><br/>
    宠物姓名:<input name="pet.name"/><br/>
    宠物年龄:<input name="pet.age"/>
    <button type="submit">submitbutton>
form>

控制器:

@RestController
public class ParameterTestController {

    /**
     * 数据绑定:页面提交的请求数据(GET、POST)都可以和对象属性进行绑定
     * @param person
     * @return
     */
    @PostMapping("/saveUser")
    public Person saveUser(Person person){
        return person;
    }
}

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

测试:http://localhost:8899 访问index.html页面,填写数据,提交表单

2)、【源码分析】

封装过程用到ServletModelAttributeMethodProcessor,这个参数处理器进行封装

public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor {
    
}

public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
    //本方法在ModelAttributeMethodProcessor类
    @Override
	public boolean supportsParameter(MethodParameter parameter) {
		return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
				(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
	}

    //本方法在ModelAttributeMethodProcessor类
	@Override
	@Nullable
	public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		...

		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 (bindingResult == null) {
			// Bean property binding and validation;
			// skipped in case of binding failure on construction.
            //============WebDataBinder============
			WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
			if (binder.getTarget() != null) {
				if (!mavContainer.isBindingDisabled(name)) {
                    //web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
					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 利用它里面的 Converters 将请求数据转成指定的数据类型,再次封装到JavaBean中;

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

3.视图解析与模板引擎

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

3.1 模板引擎-Thymeleaf

1)、thymeleaf简介

Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.

Thymeleaf 是一款现代化、服务端的Java模板引擎

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

Thymeleaf官方文档

https://www.thymeleaf.org/documentation.html

常见模板引擎有JSP、Velocity、Freemarker、Thymeleaf等,SpringBoot推荐的高级语言模板引擎Thymeleaf(一款现代化的Java服务端的模板引擎),语法更简单,功能更强大。

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

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

2)、基本语法

1、表达式

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#standard-expression-syntax

表达式名字 语法 用途
变量取值 ${…} 获取请求域、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

Simple expressions:(表达式语法)  
1.Variable Expressions: ${...}: 获取变量值 OGNL 
    	1)、获取对象的属性、调用方法  OGNL expressions
    	2)、使用内置的基本对象 Expression Basic Objects
    	#ctx : the context object.当前上下文对象
    	#vars: the context variables.当前上下文中的变量值
            #locale : the context locale.区域信息
            Web的基本对象:
		#request : (only in Web Contexts) the HttpServletRequest object.
            #response : (only in Web Contexts) the HttpServletResponse object.
            #session : (only in Web Contexts) the HttpSession object.
            #servletContext : (only in Web Contexts) the ServletContext object.
            附录Appendix A中有使用方式:例如${session.foo}从请求域中获取foo的值
        3)、内置的一些工具对象 Expression Utility Objects
        #execInfo : information about the template being processed. 
        #dates : methods for java.util.Date objects: formatting, component extraction, etc. 
        #calendars : analogous to #dates , but for java.util.Calendar objects. 
        #numbers : methods for formatting numeric objects. 
        #strings : methods for String objects: contains, startsWith, prepending/appending, etc. 
        #objects : methods for objects in general. 
        #bools : methods for boolean evaluation. 
        #arrays : methods for arrays. 
        #lists : methods for lists. 
        附录Appendix B 中有使用方式

2.Selection Variable Expressions: *{...} :选择表达式
选择表达式和${...}在功能上是一样,但是可以补充配合 th:object="${session.user}进行使用
<div th:object="${session.user}">  
    用th:object存储会话中user的值,此时*直接代表这个对象,直接使用*{ 对象的属性 }
    <p>Name: <span th:text="*{firstName}">Sebastianspan>.p>
    <p>Surname: <span th:text="*{lastName}">Pepperspan>.p>
    <p>Nationality: <span th:text="*{nationality}">Saturnspan>.p>
div>

3.Message Expressions: #{...}:获取国际化内容

4.Link URL Expressions: @{...}:定义URL
 
viewa> 

5.Fragment Expressions: ~{...}:片段引用表达式
<div th:insert="~{commons :: main}">...div>

4)、迭代 th:each

<tr th:each="prod : ${prods}">
    <td th:text="${prod.name}">Onionstd>
    <td th:text="${prod.price}">2.41td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yestd>
tr>

<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
  <td th:text="${prod.name}">Onionstd>
  <td th:text="${prod.price}">2.41td>
  <td th:text="${prod.inStock}? #{true} : #{false}">yestd>
tr>

5)、条件运算 th:if

<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">viewa>

<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administratorp>
  <p th:case="#{roles.manager}">User is a managerp>
  <p th:case="*">User is some other thingp>
div>

6)、属性优先级

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

3.2 thymeleaf使用

1)、引入Starter启动器

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

2)、自动配置了thymeleaf

在spring-boot-autoconfigure的jar包下,提供的Thymeleaf的自动配置类

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
//Thymeleaf的自动配置类
public class ThymeleafAutoConfiguration {

}

在spring-boot-autoconfigure的jar包下,提供的thymeleaf的默认规则

//定义了Thymeleaf使用的默认规则
@ConfigurationProperties( prefix = "spring.thymeleaf" )
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
	//默认的前缀
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
	//默认的后缀
    public static final String DEFAULT_SUFFIX = ".html";
}

自动配好的策略

  • 1、所有thymeleaf的配置值都在 ThymeleafProperties
  • 2、配置好了 SpringTemplateEngine
  • 3、配置好了 ThymeleafViewResolver
  • 4、我们只需要直接开发页面
// 页面模板放置处
public static final String DEFAULT_PREFIX = "classpath:/templates/"

// 文件的后缀名
public static final String DEFAULT_SUFFIX = ".html"  //xxx.html

3)、页面开发

第一步:导入Thymeleaf的名称空间

页面中需要添加如下的名称空间,则页面中使用thymeleaf时会有提示

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

第二步:编写控制层
@Controller
public class ViewTestController {
    @GetMapping("/hello")
    public String hello(Model model){
        //model中的数据会被放在请求域中
        model.addAttribute("msg","大力发展工业文化");
        model.addAttribute("link","http://www.baidu.com");
        return "success";
    }
}

第三步:使用Thymeleaf语法

页面 /templates/success.html

DOCTYPE html>

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

#设置应用名
server:
  servlet:
    context-path: /app 

# 这个设置后,URL要插入/app, 如http://localhost:8080/app/hello.html

3.3 案例-搭建后台管理系统

1)、项目创建

使用IDEA的Spring Initializr。勾选启动器:thymeleaf、web-starter、devtools、lombok。

2)、静态资源处理

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

/static 放置 css,js等静态资源

/templates/login.html 登录页

3)、路径构建

th:action="@{/login}"

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

<form class="form-signin" method="post" th:action="@{/login}">
    
    <label style="color: red" th:text="${msg}">label>

    <input type="text" name="userName" class="form-control" placeholder="User ID" autofocus>
    <input type="password" name="password" class="form-control" placeholder="Password">

    <button class="btn btn-lg btn-login btn-block" type="submit">
        <i class="fa fa-check">i>
    button>
form>

5)、页面跳转

@Controller
public class IndexController {
	
    //跳转至登录页面
    @GetMapping(value = {"/","/login"})
    public String gotoLogin(){
        //classpath:/templates/login.html
        return "login";
    }
	
    //登录功能
    @PostMapping(value = "/login")
    public String login(User user, HttpSession session ,Model model){
        if(StringUtils.hasLength(user.getName()) && StringUtils.hasLength(user.getEmail()) && "[email protected]".equals(user.getEmail()) ){
            session.setAttribute("user",user);
            return "redirect:main";
        }else{
            model.addAttribute("msg","账户与邮箱不匹配!");
            return "login";
        }
    }
	
    //跳转至主页
    @GetMapping("/main")
    public String gotoMain(HttpSession session , Model model){
        Object user = session.getAttribute("user");
        if(user ==null){
            //未登录
            model.addAttribute("msg","先登录,再访问!");
            return "login";
        }else{
            //已登录,跳转主页面main.html
            return "main";
        }
    }

    //登出
    @GetMapping("/logout")
    public String logout(HttpSession session , Model model){
        session.invalidate();
        model.addAttribute("msg","已登出!");
        return "redirect:login";
    }
}


6)、main.html 主页

thymeleaf内联写法 https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#inlining

显示登陆者信息

<p>Hello, [[${session.user.name}]]p>

登出

<li><a th:href="@{/logout}"><i class="fa fa-sign-out">i> Log Outa>li>

7)、模型

@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
    private String userName;
    private String password;
}

3.4 案例–抽取公共页面

Template Layout https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#template-layout


<foot th:fragment="copy" id="data">
  © 2011 The Good Thymes Virtual Grocery
foot>


<body>
  
  <div th:insert="footer :: copy">div>
  
  
  <div th:replace="footer :: #data">div>

  
  <div th:include="footer :: copy">div>
body>



<body>
  <div>
    <foot>
      © 2011 The Good Thymes Virtual Grocery
    foot>
  div>

  <foot>
    © 2011 The Good Thymes Virtual Grocery
  foot>

  <div>
    © 2011 The Good Thymes Virtual Grocery
  div>
body>

公共页面

  • /templates/common.html
DOCTYPE html>

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


<head th:fragment="commonheader">
    
    <link href="css/style.css" th:href="@{/css/style.css}" rel="stylesheet">
    <link href="css/style-responsive.css" th:href="@{/css/style-responsive.css}" rel="stylesheet">
    ...
head>
<body>

<div id="leftmenu" class="left-side sticky-left-side">
	...

    <div class="left-side-inner">
		...

        
        <ul class="nav nav-pills nav-stacked custom-nav">
            <li><a th:href="@{/main.html}"><i class="fa fa-home">i> <span>Dashboardspan>a>li>
            ...
            <li class="menu-list nav-active"><a href="#"><i class="fa fa-th-list">i> <span>Data Tablesspan>a>
                <ul class="sub-menu-list">
                    <li><a th:href="@{/basic_table}"> Basic Tablea>li>
                    <li><a th:href="@{/dynamic_table}"> Advanced Tablea>li>
                    <li><a th:href="@{/responsive_table}"> Responsive Tablea>li>
                    <li><a th:href="@{/editable_table}"> Edit Tablea>li>
                ul>
            li>
            ...
        ul>
        
    div>
div>




<div th:fragment="headermenu" class="header-section">
    
    <a class="toggle-btn"><i class="fa fa-bars">i>a>
    
	...
div>


    
<div id="commonscript">
    
    <script th:src="@{/js/jquery-1.10.2.min.js}">script>
    <script th:src="@{/js/jquery-ui-1.9.2.custom.min.js}">script>
    <script th:src="@{/js/jquery-migrate-1.2.1.min.js}">script>
    <script th:src="@{/js/bootstrap.min.js}">script>
    <script th:src="@{/js/modernizr.min.js}">script>
    <script th:src="@{/js/jquery.nicescroll.js}">script>
    
    <script th:src="@{/js/scripts.js}">script>
div>
body>
html>

  • /templates/table/basic_table.html
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
  <meta name="description" content="">
  <meta name="author" content="ThemeBucket">
  <link rel="shortcut icon" href="#" type="image/png">

  <title>Basic Tabletitle>
   
  
  <div th:include="common :: commonheader"> div>
head>

<body class="sticky-header">

<section>
    
	<div th:replace="common :: #leftmenu">div>
    
    
    <div class="main-content" >
		
        
        <div th:replace="common :: headermenu">div>
        ...
    div>
    
section>



<div th:include="common :: #commonscript">div>


body>
html>

3.5 案例-遍历数据与页面渲染

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#iteration

控制层代码

@GetMapping("/dynamic_table")
public String dynamic_table(Model model){
    //表格内容的遍历
    List<User> users 
        = Arrays.asList(new User("zhangsan", "123456"),
                        new User("lisi", "123444"),
                        new User("haha", "aaaaa"),
                        new User("hehe ", "aaddd"));
    model.addAttribute("users",users);
    return "table/dynamic_table";
}

页面代码


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

<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}">Tridenttd>
            <td th:text="${user.userName}">Internettd>
            <td >[[${user.password}]]td>
        tr>
    tbody>
table>

3.6 【源码分析】视图解析

原理流程

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

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

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

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

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

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

视图解析:

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

  • 返回值以 redirect: 开始: new RedirectView() --> 响应重定向

response.sendRedirect(path);

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

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

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

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

5.拦截器

5.1 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("preHandle拦截的请求路径是{}",requestURI);

        //登录检查逻辑
        HttpSession session = request.getSession();
        Object loginUser = session.getAttribute("loginUser");

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

        //拦截住。未登录。跳转到登录页
        request.setAttribute("msg","请先登录");
        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 {
        log.info("postHandle执行{}",modelAndView);
    }

    /**
     * 页面渲染以后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion执行异常{}",ex);
    }
}

5.2 配置拦截器

/**
 * 1、编写一个拦截器实现HandlerInterceptor接口
 * 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
 * 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
 */
@Configuration
public class MyWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")  //所有请求都被拦截包括静态资源
            .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //设置放行的请求
    }
}

5.3 【源码分析】拦截器原理

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

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

  • 1、如果当前拦截器preHandle返回为true。则执行下一个拦截器的preHandle
  • 2、如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的 afterCompletion;

3、如果任何一个拦截器返回false。直接跳出不执行目标方法

4、所有拦截器都返回True。执行目标方法

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

6、前面的步骤有任何异常都会直接倒序触发 afterCompletion

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

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

6.文件上传

6.1 页面表单

使用模板中的form_layouts.html页面

<form method="post" action="/upload" enctype="multipart/form-data">
    <input type="file" name="file"><br>
    <input type="submit" value="提交">
form>

6.2 文件上传代码

/**
 * MultipartFile 自动封装上传过来的文件
 * @param email
 * @param username
 * @param headerImg
 * @param photos
 * @return
 */
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
                     @RequestParam("username") String username,
                     @RequestPart("headerImg") MultipartFile headerImg,
                     @RequestPart("photos") MultipartFile[] photos) throws IOException {

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

    if(!headerImg.isEmpty()){
        //保存到文件服务器,OSS服务器
        String originalFilename = headerImg.getOriginalFilename();
        headerImg.transferTo(new File("H:\\cache\\"+originalFilename));
    }

    if(photos.length > 0){
        for (MultipartFile photo : photos) {
            if(!photo.isEmpty()){
                String originalFilename = photo.getOriginalFilename();
                photo.transferTo(new File("H:\\cache\\"+originalFilename));
            }
        }
    }

    return "main";
}

6.3 【源码分析】自动配置原理

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

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

  • 原理步骤

    • 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
  • 2、参数解析器来解析请求中的文件内容封装成MultipartFile

  • **3、将request中文件信息封装为一个Map;**MultiValueMap

FileCopyUtils。实现文件流的拷贝

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

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

7.异常处理

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-error-handling

7.1 默认规则

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

2.对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息;

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

对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据。

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9uhdhF59-1641479793656)(image/1606024592756-d4ab8a6b-ec37-426b-8b39-010463603d57.png)]

7.2 定制错误处理逻辑

1)、自定义错误页

error/404.html error/5xx.html

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

触发点:

@Controller
public class TableController {

    //触发400 Bad Request
    @GetMapping("/basic_table")
    public String basic_table(Model model, @RequestParam("a")int a){
        //模拟数据
        List<User> users
                = Arrays.asList(new User("zhangsan", "123456"),
                new User("lisi", "123444"),
                new User("haha", "aaaaa"),
                new User("hehe ", "aaddd"));

        model.addAttribute("users",users);
        return "table/basic_table";
    }
}

2)、@ControllerAdvice+@ExceptionHandler全局异常处理器

底层是 ExceptionHandlerExceptionResolver 支持的

@Slf4j
@ControllerAdvice  //增强控制器
public class GlobalExceptionhandler {
	
    //定义一个专门处理"数学异常"的处理器
    @ExceptionHandler({ArithmeticException.class})
    public String handlerArithException(Exception e){
        log.error("异常:{}",e.getMessage());
        return "login";
    }

}

触发点:

@Controller
public class TableController {

    @GetMapping("/basic_table")
    public String basic_table(Model model){
        //模拟数据
        List<User> users
                = Arrays.asList(new User("zhangsan", "123456"),
                new User("lisi", "123444"),
                new User("haha", "aaaaa"),
                new User("hehe ", "aaddd"));
		
        //触发数学异常
        int i = 10/0;
        model.addAttribute("users",users);
        return "table/basic_table";
    }
}

3)、@ResponseStatus+自定义异常

底层是 **ResponseStatusExceptionResolver支持的 **

把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);

tomcat发送的/error

//自定义异常  绑定响应错误状态码
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public class CustomException extends RuntimeException {
    public CustomException() {
        super();
    }

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

触发点:

@Controller
public class TableController {

    @GetMapping("/basic_table")
    public String basic_table(Model model){
        //模拟数据
        List<User> users
                = Arrays.asList(new User("zhangsan", "123456"),
                new User("lisi", "123444"),
                new User("haha", "aaaaa"),
                new User("hehe ", "aaddd"));
		
        //触发自定义异常
        if(users.size()>3){
            throw new CustomException("太多值");
        }
        model.addAttribute("users",users);
        return "table/basic_table";
    }
}

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

底层是DefaultHandlerExceptionResolver支持的,处理框架底层的异常(例如:400 缺失请求参数)

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

此次请求立即结束,tomcat发送的/error,被BasicErrorController控制器处理。

tomcat原生的错误页:

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

5)、自定义实现 HandlerExceptionResolver 处理异常

可以作为默认的全局异常处理规则

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qHAxu7iE-1641479793657)(image/1606114688649-e6502134-88b3-48db-a463-04c23eddedc7.png)]

//设置优先级,数字越小优先级越高
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
public class CustomHandlerExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request,
                                         HttpServletResponse response,
                                         Object handler,
                                         Exception ex) {
        try {
            response.sendError(422,"自定义的错误");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ModelAndView();
    }
}

6)、ErrorViewResolver 实现自定义处理异常

1.只要执行response.sendError() ,error请求就会转给controller;

2.出现的异常没有任何人能处理,tomcat底层也是 response.sendError(),error请求就会转给controller;

3.BasicErrorController 要跳转的页面地址是由 ErrorViewResolver视图解析器解析的

7.3 【源码分析】异常处理自动配置原理

  • ErrorMvcAutoConfiguration 自动配置异常处理规则

    • 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
      • public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
      • DefaultErrorAttributes:定义错误页面中可以包含哪些数据。

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

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

    • **容器中的组件:类型:**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】。(默认是一个白页)

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

7.4 【源码分析】异常处理步骤流程

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

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

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

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BXuCwLix-1641479793660)(image/1606047109161-c68a46c1-202a-4db1-bbeb-23fcae49bbe9.png)]

    • 1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
    • 2、默认没有任何人能处理异常,所以异常会被抛出
      • 1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
      • 2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xfxaONeC-1641479793660)(image/1606047900473-e31c1dc3-7a5f-4f70-97de-5203429781fa.png)]

      • 3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
      • 4、模板引擎最终响应这个页面 error/500.html

8.Web原生组件注入(Servlet、Filter、Listener)

官网:提供嵌入式容器的支持

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-embedded-container

8.1 使用Servlet API

推荐使用以下方式

//在主配置类中,使用注解指定扫描Servlet的包,指定原生Servlet组件都放在那里
@ServletComponentScan(basePackages = "com.igeek")

//在自定义的Servlet中,需要继承HttpServlet,并基于Servlet3.0后支持使用的注解标识
@WebServlet(urlPatterns = "/my")  //效果:直接响应,没有经过Spring的拦截器

//自定义的过滤器
@WebFilter(urlPatterns={"/css/*","/images/*"})

//自定义的监听器
@WebListener

8.2 使用RegistrationBean

ServletRegistrationBean, FilterRegistrationBean, and ServletListenerRegistrationBean

@Configuration
public class MyRegistConfig {

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


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

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

8.3 【源码分析】DispatchServlet 如何注册

扩展:DispatchServlet 如何注册进来?

  • 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。
  • 通过 ServletRegistrationBean 把 DispatcherServlet 配置进来。
  • 默认映射的是 / 路径。

Tomcat-Servlet处理原则:

多个Servlet都能处理到同一层路径时,会精确优选原则,去匹配执行。

9.定制化原理

9.1 定制化的常见方式

方式一:修改配置文件

方式二:XxxCustomizer 定制化器

方式三:编写自定义的配置类 xxxConfiguration + @Bean替换、增加容器中默认组件、视图解析器

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

@Configuration
public class MyWebConfig 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 没有生效。

9.2 原理分析套路

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

五.数据访问

1.SQL

1.1 数据源的自动配置-HikariDataSource

1)、导入JDBC场景

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-jdbcartifactId>
dependency>

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

导入数据库驱动

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

官方不知道我们接下要操作什么数据库。


<mysql.version>8.0.22mysql.version>


<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    
dependency>


<properties>
    <java.version>1.8java.version>
    <mysql.version>5.1.49mysql.version>
properties>

2)、分析自动配置

  • DataSourceAutoConfiguration : 数据源的自动配置

    • 修改数据源相关的配置:spring.datasource
    • 数据库连接池的配置,是自己容器中没有DataSource才自动配置的
    • 底层配置好的连接池是:HikariDataSource
@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{

}

  • DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置

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

    • 可以通过这个配置项@ConfigurationProperties(prefix = “spring.jdbc”) JdbcProperties 来修改JdbcTemplate
    • @Bean@Primary JdbcTemplate;容器中有这个组件
  • JndiDataSourceAutoConfiguration: jndi的自动配置

  • XADataSourceAutoConfiguration: 分布式事务相关的

3)、修改配置项

mysql5.5版本

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

mysql8版本

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/testspring?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

4)、测试

@Slf4j
@SpringBootTest
class Boot05WebAdminApplicationTests {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Test
    void contextLoads() {
        Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class);
        log.info("记录总数:{}",aLong);
    }

}

1.2 使用Druid数据源

1)、druid官方github地址

https://github.com/alibaba/druid

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

  • 自定义
  • starter

2)、自定义方式

第一步:切换数据源DruidDataSource

https://mvnrepository.com/artifact/com.alibaba/druid


<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>druidartifactId>
    <version>1.1.22version>
dependency>

以前在xml配置文件中,做如下配置:


<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
		destroy-method="close">
		<property name="url" value="${jdbc.url}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
		<property name="maxActive" value="20" />
		<property name="initialSize" value="1" />
		<property name="maxWait" value="60000" />
		<property name="minIdle" value="1" />
		<property name="timeBetweenEvictionRunsMillis" value="60000" />
		<property name="minEvictableIdleTimeMillis" value="300000" />
		<property name="testWhileIdle" value="true" />
		<property name="testOnBorrow" value="false" />
		<property name="testOnReturn" value="false" />
		<property name="poolPreparedStatements" value="true" />
		<property name="maxOpenPreparedStatements" value="20" />
bean>

现在可以思考下,SpringBoot中如何配置DruidDataSource组件?

配置文件@Configuration + @Bean组合方式,配置DruidDataSource数据源

@Configuration
public class MyDruidDataSourceConfig {

    /**
     * 1.@Bean注解
     * DruidDataSource组件配置在IOC容器中
     *
     * 2.@ConfigurationProperties(prefix = "spring.datasource")
     * 配置绑定,application.yml文件中设置的数据源的相关配置可以直接使用
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        return dataSource;
    }

}

第二步:StatViewServlet

StatViewServlet的用途包括:

  • 提供监控信息展示的html页面
  • 提供监控信息的JSON API

以前在web.xml配置文件中,做如下配置:

<servlet>
    <servlet-name>DruidStatViewservlet-name>
    <servlet-class>com.alibaba.druid.support.http.StatViewServletservlet-class>
servlet>
<servlet-mapping>
    <servlet-name>DruidStatViewservlet-name>
    <url-pattern>/druid/*url-pattern>
servlet-mapping>

现在可以思考下,SpringBoot中如何配置StatViewServlet组件?

配置文件@Configuration + @Bean组合方式,配置StatViewServlet数据源

@Configuration
public class MyDruidDataSourceConfig {

    /**
     * 1.@Bean注解
     * DruidDataSource组件配置在IOC容器中
     *
     * 2.@ConfigurationProperties(prefix = "spring.datasource")
     * 配置绑定,application.yml文件中设置的数据源的相关配置可以直接使用
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        return dataSource;
    }

    //配置StatViewServlet  提供监控信息展示的html页面
    @Bean
    public ServletRegistrationBean<StatViewServlet> servletRegistrationBean(){
        StatViewServlet statViewServlet = new StatViewServlet();
        ServletRegistrationBean<StatViewServlet> bean =
                new ServletRegistrationBean<>(statViewServlet,"/druid/*");
        //配置上登录的用户名和密码
        bean.addInitParameter("loginUsername","admin");
        bean.addInitParameter("loginPassword","admin");
        //禁止清空统计数据
        bean.addInitParameter("resetEnable","false");
        return bean;
    }
}

第三步:StatFilter

用于统计监控信息;如SQL监控、URI监控


<property name="filters" value="stat,slf4j" />

系统中所有filter:

别名 Filter类名
default com.alibaba.druid.filter.stat.StatFilter
stat com.alibaba.druid.filter.stat.StatFilter
mergeStat com.alibaba.druid.filter.stat.MergeStatFilter
encoding com.alibaba.druid.filter.encoding.EncodingConvertFilter
log4j com.alibaba.druid.filter.logging.Log4jFilter
log4j2 com.alibaba.druid.filter.logging.Log4j2Filter
slf4j com.alibaba.druid.filter.logging.Slf4jLogFilter
commonlogging com.alibaba.druid.filter.logging.CommonsLogFilter
/**
     * 1.@Bean注解
     * DruidDataSource组件配置在IOC容器中
     *
     * 2.@ConfigurationProperties(prefix = "spring.datasource")
     * 配置绑定,application.yml文件中设置的数据源的相关配置可以直接使用
     */
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() throws SQLException {
    DruidDataSource dataSource = new DruidDataSource();
    //开启SQL监控、防火墙监控、日志监控
    dataSource.setFilters("stat,wall,slf4j");
    return dataSource;
}

第四步:WebStatFilter

用于采集web-jdbc关联监控的数据。

配置文件@Configuration + @Bean组合方式,配置WebStatFilter数据源。

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

通过访问:http://localhost:8080/druid/datasource.html 查看druid对SQL监控情况

3)、使用官方starter方式

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

第一步:引入druid-starter
<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>druid-spring-boot-starterartifactId>
    <version>1.1.17version>
dependency>

第二步:分析自动配置
  • DruidDataSourceAutoConfigure.class,扩展配置项 spring.datasource.druid
  • DruidSpringAopConfiguration.class, 监控SpringBean的;配置项:spring.datasource.druid.aop-patterns
  • DruidStatViewServletConfiguration.class, 监控页的配置:spring.datasource.druid.stat-view-servlet;默认开启
  • DruidWebStatFilterConfiguration.class, web监控配置;spring.datasource.druid.web-stat-filter;默认开启
  • DruidFilterConfiguration.class, 所有Druid自己filter的配置
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";

第三步:配置示例
spring:
  # 配置数据源的基础信息
  datasource:
    #JDBC属性配置
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?unicode=true&characterEncode=utf8&serverTimezone=GMT%2B8
    username: root
    password: root

    #针对于Druid数据源的配置
    druid:
      #监控SpringBean,开启Spring监控
      aop-patterns: com.igeek.springboot.*
      #开启SQL监控、防火墙监控、日志监控
      filters: stat,wall,slf4j

      #用于采集web-jdbc关联监控的数据
      web-stat-filter:
        enabled: true   #开启web监控
        url-pattern: /*
        exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"

      #提供监控信息展示的html页面
      stat-view-servlet:
        enabled: true  #开启html页面展示
        url-pattern: /druid/*
        login-username: admin
        login-password: 123
        reset-enable: false #禁止清空统计数据按钮

      #细节配置
      filter:
        #stat SQL监控,对上面filters里面的stat的详细配置
        stat:
          enabled: true   #开启SQL监控
          log-slow-sql: true  #开启慢日志监控
          slow-sql-millis: 1000 #单位为ms,SQL执行多久算慢,当前设置为sql执行>1s就算慢
        #wall 防火墙,对上面filters里面的wall的详细配置
        wall:
          enabled: true
          config:
            selelct-allow: false #禁止查询
            alter-table-allow: false #禁止修改表
            drop-table-allow: true #允许删除表

配置项列表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

1.3 整合MyBatis操作

https://github.com/mybatis

starter

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

第三方的: *-spring-boot-starter

<dependency>
    <groupId>org.mybatis.spring.bootgroupId>
    <artifactId>mybatis-spring-boot-starterartifactId>
    <version>2.1.4version>
dependency>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BIB6ws4r-1641479793661)(image/1606704096118-53001250-a04a-4210-80ee-6de6a370be2e.png)]

1)、配置模式

  • 全局配置文件
  • SqlSessionFactory: 自动配置好了
  • SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession
  • @Import(AutoConfiguredMapperScannerRegistrar.class);
  • Mapper: 只要我们写的操作MyBatis的接口标准了 @Mapper 就会被自动扫描进来
@EnableConfigurationProperties(MybatisProperties.class) // MyBatis配置项绑定类。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration{
    
}

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

可以修改配置文件中 mybatis 开始的所有:

# 配置mybatis规则
mybatis:
  config-location: classpath:mybatis/mybatis-config.xml  #全局配置文件位置
  mapper-locations: classpath:mybatis/mapper/*.xml  #sql映射文件位置



DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.igeek.springboot.mapper.UserMapper">
    <select id="getUser" paramterType="int" resultType="com.igeek.springboot.bean.User">
        select * from  user where  id=#{id}
    select>
mapper>

配置 private Configuration configuration; mybatis.configuration下面的所有,就是相当于改mybatis全局配置文件中的值

# 配置mybatis规则
mybatis:
  #config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true  #开启驼峰式命名
    
#可以不写全局;配置文件,所有全局配置文件的配置都放在configuration配置项中即可

整合配置模式版,开发步骤:

  • 导入mybatis官方starter

  • 编写mapper接口。标准@Mapper注解

  • 编写sql映射文件并绑定mapper接口

  • 在application.yaml中指定Mapper配置文件的位置,以及指定全局配置文件的信息 (建议配置在mybatis.configuration

2)、注解模式

@Mapper
public interface CityMapper {

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

    public void insert(City city);

}

3)、混合模式

@Mapper
public interface CityMapper {

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

    public void insert(City city);

}

最佳实战:

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

1.4 整合MyBatis-Plus

1)、什么是MyBatis-Plus

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

mybatis plus 官网

建议安装 MybatisX 插件

特点:

润物无声

只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑。

效率至上

只需简单配置,即可快速进行 CRUD 操作,从而节省大量时间。

丰富功能

热加载、代码生成、分页、性能分析等功能一应俱全。

2)、整合MyBatis-Plus

第一步:引入依赖

<dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-boot-starterartifactId>
    <version>3.4.1version>
dependency>

注意:引入 MyBatis-Plus 之后请不要再次引入 MyBatis,以避免因版本差异导致的问题。mysql依赖需要自己引入。

第二步:了解自动配置情况
  • MybatisPlusAutoConfiguration 配置类,MybatisPlusProperties 配置项绑定。mybatis-plus:xxx 就是对mybatis-plus的定制
  • SqlSessionFactory 自动配置好。底层是容器中默认的数据源
  • mapperLocations 自动配置好的。有默认值。classpath*:/mapper/**/*.xml;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下。
  • 容器中也自动配置好了SqlSessionTemplate会话对象
  • @Mapper 标注的接口也会被自动扫描;或者在主配置类上直接 添加@MapperScan(“com.igeek.springboot.mapper”) 批量扫描。
第三步:配置文件

在 application.properties或者yaml 配置文件中添加 MySQL 数据库的相关配置:

application.properties

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?unicode=true&characterEncode=utf8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root

application.yml

spring:
  # 配置数据源的基础信息
  datasource:
    #JDBC属性配置
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?unicode=true&characterEncode=utf8&serverTimezone=GMT%2B8
    username: root
    password: root


注意:

1、这里的 url 使用了 ?serverTimezone=GMT%2B8 后缀,因为8.0版本的jdbc驱动需要添加这个后缀,否则运行测试用例报告如下错误:

java.sql.SQLException: The server time zone value ‘Öйú±ê׼ʱ¼ä’ is unrecognized or represents more

2、这里的 driver-class-name 使用了 com.mysql.cj.jdbc.Driver ,在 jdbc 8 中 建议使用这个驱动,否则运行测试用例的时候会有 WARN 信息

第四步:启动类

在 Spring Boot 启动类中添加 @MapperScan 注解,扫描 Mapper 文件夹

@SpringBootApplication
@MapperScan("com.igeek.springboot.mapper")
public class DemomptestApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemomptestApplication.class, args);
    }
}

第五步:实体类

创建包 entity 编写实体类 User.java(此处使用了 Lombok 简化代码)

表数据在官网上

@Data
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

第六步:mapper映射

创建包 mapper 编写Mapper 接口

UserMapper.java

@Repository
public interface UserMapper extends BaseMapper<User> {
    
}

第七步:service业务

创建包 service编写IService 接口UserService.java

public interface UserService extends IService<User> {

}

创建包 service编写UserServiceImpl实现类

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {

}

第八步:查看sql输出日志
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

第九步:测试
@SpringBootTest
class SpringbootCh05MybatisplusApplicationTests {

    @Autowired
    private UserService userService;

    //查询列表
    @Test
    public void testSelectAll(){
        //SELECT id,name,age,email FROM user
        List<User> list = userService.list(null);
        System.out.println(list);
    }
}

3)、CRUD - 查询

1.查询
1.1 通过多个id批量查询

完成了动态sql的foreach的功能

//通过多个id批量查询
@Test
public void testSelectByIds(){
    //SELECT id,name,age,email FROM user WHERE id IN ( ? , ? , ? )
    List<User> list = userService.listByIds(Arrays.asList(1,2,3));
    System.out.println(list);
}

1.2 简单的条件查询

通过map封装查询条件

注意:map中的key对应数据库中的列名。如:数据库user_id,实体类是userId,这时map的key需要填写user_id

//简单的条件查询
@Test
public void testSelectByMap(){
    //Map集合的key一定要与表中字段一致
    Map<String,Object> map = new HashMap();
    map.put("name","Jone");
    map.put("age",18);
    //SELECT id,name,age,email FROM user WHERE name = ? AND age = ?
    List<User> list = userService.listByMap(map);
    System.out.println(list);
}

2.分页
2.1 分页插件

MyBatis Plus自带分页插件,只要简单的配置即可实现分页功能

2.1.1 添加分页插件

配置类中添加@Bean配置

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {

    // 将分页插件加入至IOC容器中
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //添加内部拦截器
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
        // paginationInnerInterceptor.setOverflow(false);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        paginationInnerInterceptor.setMaxLimit(-1L);
        interceptor.addInnerInterceptor(paginationInnerInterceptor);
        return interceptor;
    }

}

2.1.2 测试selectPage分页

测试:最终通过page对象获取相关数据

//分页查询
@Test 
public void testSelectPage() {   
    Page<User> page = new  Page(1,3);   
    //返回对象得到分页所有数据
    Page<User> userPage =  userMapper.selectPage(page, null);  
    //总页数
    long pages = userPage.getPages(); 
    //当前页  
    long current = userPage.getCurrent(); 
    //查询数据集合
    List<User> records = userPage.getRecords(); 
    //总记录数
    long total = userPage.getTotal(); 
    //下一页
    boolean hasNext = userPage.hasNext();  
    //上一页
    boolean hasPrevious = userPage.hasPrevious(); 
    
    System.out.println(pages);   
    System.out.println(current);   
    System.out.println(records);   
    System.out.println(total);   
    System.out.println(hasNext);   
    System.out.println(hasPrevious); 
}

2.1.3 测试selectMapsPage分页

当指定了特定的查询列时,希望分页结果列表只返回被查询的列,而不是很多null值

测试selectMapsPage分页:结果集是Map

@Test
public void testSelectMapsPage() {
    //Page不需要泛型
    Page<Map<String, Object>> page = new Page<>(1, 5);
    Page<Map<String, Object>> pageParam = userMapper.selectMapsPage(page, null);
    List<Map<String, Object>> records = pageParam.getRecords();
    records.forEach(System.out::println);
    System.out.println(pageParam.getCurrent());
    System.out.println(pageParam.getPages());
    System.out.println(pageParam.getSize());
    System.out.println(pageParam.getTotal());
    System.out.println(pageParam.hasNext());
    System.out.println(pageParam.hasPrevious());
}

2.2 Controller控制层
//分页查询
@GetMapping("/dynamic_table")
public String findAllByPage(
    @RequestParam(value="now",defaultValue="1") Integer pageNow,
    Model model
){
    //封装Page对象 第一个参数:当前页码  第二个参数:每页展示多少条记录
    IPage page = new Page(pageNow,2);
    //Service提供page来进行分页
    IPage userPage = userService.page(page);
    /**
     * 将数据放至请求域
     * records  记录列表
     * total  总记录数
     * size 每页显示条数
     * current 当前页
     * pages  总页数
     */
    model.addAttribute("page",userPage);
    return "table/dynamic_table";
}

2.3 页面
<tbody>
    <tr class="gradeX" th:each="user : ${page.records}">
        <td>[[${user.id}]]td>
        <td th:text="${user.name}">Internettd>
        <td th:text="${user.age}">Win 95+td>
        <td th:text="${user.email}">4td>
        <td>
            <input type="button" class="btn btn-danger" value="删除" /> |
            <input type="button" class="btn btn-warning" value="修改" />
        td>
    tr>
tbody>

<div class="span6">
    <div class="dataTables_info" id="dynamic-table_info">
        总[[${page.pages}]]页数,总共[[${page.total}]]记录数,
        当前第[[${page.current}]]页,每页显示[[${page.size}]]记录
    div>
div>

<div class="dataTables_paginate paging_bootstrap pagination">
    <ul>
        <li class="prev disabled" th:class="${page.current eq 1}?'prev disabled':'prev'">
            <a th:href="@{/dynamic_table(now=${page.current}-1)}">← Previousa>
        li>
        <li class="active" th:class="${now eq page.current}?'active':''"
            th:each="now : ${#numbers.sequence(1,page.pages)}">
            <a th:href="@{/dynamic_table(now=${now})}">[[${now}]]a>
        li>
        <li class="next" th:class="${page.current eq page.pages}?'next disabled':'next'">
            <a th:href="@{/dynamic_table(now=${page.current}+1)}">Next → a>
        li>
    ul>
div>

4)、CRUD功能 - 添加

1.MP的主键策略
ASSIGN_ID 策略

MyBatis-Plus默认的主键策略是:ASSIGN_ID (使用了雪花算法)

@TableId(type = IdType.ASSIGN_ID)
private String id;

雪花算法:分布式ID生成器

雪花算法是由Twitter公布的分布式主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的主键的有序性。

核心思想:

长度共64bit(一个long型)。

首先是一个符号位,1bit标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0。

41bit时间截(毫秒级),存储的是时间截的差值(当前时间截 - 开始时间截),结果约等于69.73年。

10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID,可以部署在1024个节点)。

12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID)。

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

优点:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率较高。

AUTO 自增策略

需要在创建数据表的时候设置主键自增

实体字段中配置 @TableId(type = IdType.AUTO)

@TableId(type = IdType.AUTO)
private Long id;

要想影响所有实体的配置,可以设置全局主键配置

#全局设置主键生成策略
mybatis-plus.global-config.db-config.id-type=auto

2.插入操作
//添加
@Test
public void testAdd() {
    User user = new User();
    user.setName("lucy");
    user.setAge(20);
    user.setEmail("[email protected]");
    int insert = userMapper.insert(user);
    System.out.println(insert);
}

5)、CRUD功能 - 删除

1.物理删除
1.1 根据id删除记录
@Test
public void testDeleteById(){
    int result = userMapper.deleteById(5L);
    system.out.println(result);
}

1.2 批量删除
@Test
public void testDeleteBatchIds() {
    int result = userMapper.deleteBatchIds(Arrays.asList(8, 9, 10));
	system.out.println(result);
}

1.3 简单条件删除
@Test
public void testDeleteByMap() {
    HashMap<String, Object> map = new HashMap<>();
    map.put("name", "Helen");
    map.put("age", 18);
    int result = userMapper.deleteByMap(map);
    system.out.println(result);
}

Controller层

@Autowired
private UserService userService;

@GetMapping("/user/delete/{id}")
public String deleteUser(@PathVariable("id") Long id,
                         @RequestParam(value = "pn",defaultValue = "1")Integer pn,
                         RedirectAttributes ra){

    userService.removeById(id);

    ra.addAttribute("pn",pn);
    return "redirect:/dynamic_table";
}

2.逻辑删除
2.1 物理删除和逻辑删除

**物理删除:**真实删除,将对应数据从数据库中删除,之后查询不到此条被删除数据

**逻辑删除:**假删除,将对应数据中代表是否被删除字段状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录

逻辑删除的使用场景:

可以进行数据恢复

有关联数据,不便删除

分析客户的行为

2.2 逻辑删除实现流程

2.2.1 数据库修改

添加 deleted字段

ALTER TABLE `user` ADD COLUMN `deleted` int DEFAULT 0;

2.2.2 实体类修改

添加deleted 字段,并加上 @TableLogic 注解

@TableLogic
private Integer deleted;

2.2.3 配置(可选)

application.properties 加入以下配置,此为默认值,如果你的默认值和mp默认的一样,该配置可无

mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0

2.2.4 测试

测试后发现,数据并没有被删除,deleted字段的值由0变成了1

测试后分析打印的sql语句,是一条update

注意:被删除前,数据的deleted 字段的值必须是 0,才能被选取出来执行逻辑删除的操作

@Test
public void testLogicDelete() {  
    int result = userMapper.deleteById(1L);
    system.out.println(result);
}

2.2.5 测试逻辑删除后的查询

MyBatis Plus中查询操作也会自动添加逻辑删除字段的判断

@Test
public void testLogicDeleteSelect() {
    List<User> users = userMapper.selectList(null);
    users.forEach(System.out::println);
} 

6)、CRUD功能 - 修改(自动填充和乐观锁)

1.更新操作

注意:update时生成的sql自动是动态sql

UPDATE user SET age=? WHERE id=? 

//修改
@Test 
public void testUpdate() {   
    User user = new User();   
    user.setId(1340868235401764865L);   
    user.setName("lucymary");   
    int count = userMapper.updateById(user);   
    System.out.println(count); 
}

2.自动填充

需求描述:

项目中经常会遇到一些数据,每次都使用相同的方式填充,例如记录的创建时间,更新时间等。

我们可以使用MyBatis Plus的自动填充功能,完成这些字段的赋值工作

1.1 数据库修改

在User表中添加datetime类型的新的字段 create_time、update_time

1.2 实体类修改

实体上增加字段并添加自动填充注解

//create_time
@TableField(fill = FieldFill.INSERT) 
private Date createTime;  

//update_time
@TableField(fill = FieldFill.INSERT_UPDATE) 
private Date updateTime; 

1.3 实现元对象处理器接口

注意:不要忘记添加 @Component 注解

@Component 
public class MyMetaObjectHandler implements MetaObjectHandler {    
    //mp执行添加操作,这个方法执行   
    @Override   
    public void insertFill(MetaObject metaObject) {     
        this.setFieldValByName("createTime",new Date(),metaObject);     
        this.setFieldValByName("updateTime",new Date(),metaObject);   
                                                  
    }    
    
    //mp执行修改操作,这个方法执行   
    @Override   
    public void updateFill(MetaObject metaObject) {     
        this.setFieldValByName("updateTime",new Date(),metaObject);   
    } 
}

3、乐观锁
场景

主要适用场景:当要更新一条记录的时候,希望这条记录没有被别人更新,也就是说实现线程安全的数据更新

乐观锁实现方式:

1.取出记录时,获取当前version

2.更新时,带上这个version

3.执行更新时, set version = newVersion where version = oldVersion

4.如果version不对,就更新失败

4、乐观锁实现流程
4.1 修改实体类

添加 @Version 注解

@Version
private Integer version;

4.2 创建配置文件

创建包config,创建文件MybatisPlusConfig.java

此时可以删除主类中的 @MapperScan 扫描注解

@Configuration
public class MyConfig {

    // 将插件加入至IOC容器中
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        //添加乐观锁插件
        OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor = new OptimisticLockerInnerInterceptor();
        interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor);
        return interceptor;
    }

}

7)、条件构造器和常用接口

1.wrapper介绍

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

Wrapper : 条件构造抽象类,最顶端父类

​ AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件

​ QueryWrapper : 查询条件封装

​ UpdateWrapper : Update 条件封装

AbstractLambdaWrapper : 使用Lambda 语法

​ LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper

​ LambdaUpdateWrapper : Lambda 更新封装Wrapper

@SpringBootTest
public class QueryWrapperTests { 
    @Autowired
    private UserMapper userMapper;
}

2.测试用例
2.1 ge、gt、le、lt、isNull、isNotNull
@Test
public void testQuery() {
    QueryWrapper<User>queryWrapper = newQueryWrapper<>();
    queryWrapper.isNull("name").ge("age", 12).isNotNull("email");  
    int result = userMapper.delete(queryWrapper);
    System.out.println("delete return count = " + result);
}

2.2 eq、ne

注意:seletOne()返回的是一条实体记录,当出现多条时会报错

@Test
public void testSelectOne() {
    QueryWrapper<User>queryWrapper = newQueryWrapper<>();
    queryWrapper.eq("name", "Tom");
    Useruser = userMapper.selectOne(queryWrapper);
    //只能返回一条记录,多余一条则抛出异常
    System.out.println(usr);
} 

2.3 between、notBetween

包含大小边界

@Test
public void testSelectCount() {
    QueryWrapper<User>queryWrapper = newQueryWrapper<>();
    queryWrapper.between("age", 20, 30);  
    Integer count = userMapper.selectCount(queryWrapper); 
    //返回数据数量
    System.out.println(count);
}

2.4 like、notLike、likeLeft、likeRight

selectMaps()返回Map集合列表,通常配合select()使用

@Test
public void testSelectMaps() {
    QueryWrapper<User>queryWrapper = newQueryWrapper<>();
    queryWrapper.select("name", "age").like("name", "e").likeRight("email", "5");
    List<Map<String, Object>>maps = userMapper.selectMaps(queryWrapper);
    //返回值是Map列表
    maps.forEach(System.out::println);
}

2.5 orderBy、orderByDesc、orderByAsc
@Test
public void testSelectListOrderBy() {
    QueryWrapper<User>queryWrapper = newQueryWrapper<>();
    ueryWrapper.orderByDesc("age", "id");
    List<User>users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
}

3.查询方式
查询方式 说明
setSqlSelect 设置 SELECT 查询字段
where WHERE 语句,拼接 + WHERE 条件
and AND 语句,拼接 + AND 字段=值
andNew AND 语句,拼接 + AND (字段=值)
or OR 语句,拼接 + OR 字段=值
orNew OR 语句,拼接 + OR (字段=值)
eq 等于=
allEq 基于 map 内容等于=
ne 不等于<>
gt 大于>
ge 大于等于>=
lt 小于<
le 小于等于<=
like 模糊查询 LIKE
notLike 模糊查询 NOT LIKE
in IN 查询
notIn NOT IN 查询
isNull NULL 值查询
isNotNull IS NOT NULL
groupBy 分组 GROUP BY
having HAVING 关键词
orderBy 排序 ORDER BY
orderAsc ASC 排序 ORDER BY
orderDesc DESC 排序 ORDER BY
exists EXISTS 条件语句
notExists NOT EXISTS 条件语句
between BETWEEN 条件语句
notBetween NOT BETWEEN 条件语句
addFilter 自由拼接 SQL
last 拼接在最后,例如:last(“LIMIT 1”)

2.NoSQL

​ Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

2.1 Redis自动配置

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-14GpRXjQ-1641479793663)(image/34.jpg)]

自动配置:

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

redis环境搭建

1、阿里云按量付费redis。经典网络

2、申请redis的公网连接地址

3、修改白名单 允许0.0.0.0/0 访问

2.2 RedisTemplate与Lettuce

@Test
void testRedis(){
    ValueOperations<String, String> operations = redisTemplate.opsForValue();

    operations.set("hello","world");

    String hello = operations.get("hello");
    System.out.println(hello);
}

2.3 切换至jedis

第一步 引入jedis的依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>


<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
dependency>

第二步 加强连接redis的客户端类型为jedis

spring:
  redis:
      host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com
      port: 6379
      password: root:123456
      client-type: jedis
      jedis:
        pool:
          max-active: 10

连接redis失败的解决办法:
1.查看有没有启动Redis服务器。

2.redis的配置application.yml(或application.properties)中
spring.redis.timeout连接超时时间(毫秒)中设置不能为0,
一般修改如下:spring.redis.timeout=5000。

3.找到redis的配置文件 redis.conf : 执行 vim redis.conf
3.1 protected-mode yes 改为 protected-mode no (即该配置项表示是否开启保护模式,默认是开启,开启后Redis只会本地进行访问,拒绝外部访问)。
3.2 注释掉 bin127.0.0.1 即 #bin 127.0.0.1 (ps: 不注释掉,表示指定 redis 只接收来自于该 IP 地址的请求,注释掉后,则表示将处理所有请求)。

​ 3.3 daemonize yes

4.如果在Redis中没有配置requirepass ,那么在application.properties(或application.yaml)中就不要写spring.redis.password。

你可能感兴趣的:(spring,spring-boot,spring,spring,boot)