Spring Boot 总结

1. 概念

1.1 Spring Boot 优点

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run".

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

  • 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

Spring Boot 是整合 Spring 技术栈的一站式框架,简化 Spring 技术栈的快速开发脚手架

1.2 Spring Boot 缺点

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

1.3 微服务概念

James Lewis and Martin Fowler (2014) 提出微服务完整概念:https://martinfowler.com/microservices/

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 交互
  • 服务围绕业务功能拆分
  • 可以由全自动部署机制独立部署
  • 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术

1.4 分布式

困难:

  • 远程调用
  • 服务发现
  • 负载均衡
  • 服务容错
  • 配置管理
  • 服务监控
  • 链路追踪
  • 日志管理
  • 任务调度
  • ......

解决:Spring Boot + Spring Cloud

1.5 云原生(Cloud Native)

原生应用如何上云。困难:

  • 服务自愈
  • 弹性伸缩
  • 服务隔离
  • 自动化部署
  • 灰度发布
  • 流量治理
  • ......

2. 基础使用

1.1 POM 依赖

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

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

1.2 创建主程序

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

1.3 配置文件 application.yml

server:
  port: 8081

1.4 控制器

@Controller
public class TestController {
    @RequestMapping("/index")
    @ResponseBody
    public String index(){
        return "Hello SpringBoot";
    }
}

1.6 启动

直接运行 SpringbootTestApplication 主程序的 Main 方法

1.7 简化部署

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

把项目打成jar包,直接在目标服务器执行即可。java -jar SpringbootTest-0.0.1-SNAPSHOT.jar

2. 自动配置

2.1 依赖管理

父项目做依赖管理

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

它的父项目
<parent>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.6.4</version>
</parent>

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

2.2 开发导入 starter 场景启动器

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

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

  1. 引入依赖默认都可以不写版本
  2. 引入非版本仲裁的 jar,要写版本号
  3. 可以修改默认版本号
1、查看 spring-boot-dependencies 里面规定当前依赖的版本用的 key。
2、在当前项目里面重写配置
<properties>
	<mysql.version>8.0.19</mysql.version>
</properties>

2.4 自动配置

  • 自动配好 Tomcat

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

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

    • Spring Boot 配置好了所有 Web 开发的常见场景
  • 默认的包结构

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

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

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

2.5 自动配置原理

2.5.1 引导加载自动配置类

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication{
}
  1. @SpringBootConfiguration
    @Configuration。代表当前是一个配置类

  2. @ComponentScan
    指定扫描哪些 Spring 注解

  3. @EnableAutoConfiguration

    @AutoConfigurationPackage
    @Import(AutoConfigurationImportSelector.class)
    public @interface EnableAutoConfiguration {
    }
    
    • @AutoConfigurationPackage
      自动配置包,指定了默认的包规则
      @Import(AutoConfigurationPackages.Registrar.class)  // 给容器中导入一个组件
      public @interface AutoConfigurationPackage {
      }
      
      // 利用 Registrar 给容器中导入一系列组件
      // 将指定的一个包下的所有组件导入进来,MainApplication 所在包下
      
    • @Import(AutoConfigurationImportSelector.class)
      • 利用 getAutoConfigurationEntry(annotationMetadata); 给容器中批量导入一些组件
      • 调用 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes) 获取到所有需要导入到容器中的配置类
      • 利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader); 得到所有的组件
      • META-INF/spring.factories 位置来加载一个文件
        默认扫描当前系统里面所有 META-INF/spring.factories 位置的文件
        spring-boot-autoconfigure-2.6.4.jar 包里面也有 META-INF/spring.factories

文件里面写死了 Spring Boot 一启动就要给容器中加载的所有配置类

spring-boot-autoconfigure-2.3.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,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration

2.5.2 按需开启自动配置

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

2.5.3 修改默认配置

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

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

@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
}

2.6 小结

  • Spring Boot 先加载所有的自动配置类 xxxxxAutoConfiguration
  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties 里面拿xxxProperties 和配置文件进行了绑定
  • 生效的配置类就会给容器中装配组件
  • 只要容器中有这些组件,相当于拥有对应的这些功能
  • 定制化配置
    • 用户直接自己 @Bean 替换底层的组件
    • 用户去看这个组件是获取的配置文件什么值就去修改

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

最佳实践

  1. 引入场景依赖
    https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter
  2. 查看自动配置了哪些(选做)
    • 自己分析,引入场景对应的自动配置一般都生效了
    • 配置文件中 debug=true 开启自动配置报告。Negative(不生效)\Positive(生效)
  3. 是否需要修改

3. 注解与配置绑定

3.1 @Configuration(proxyBeanMethods)

告诉Spring Boot这是一个配置类 == 配置文件,配置类本身也是组件。配置类里面使用 @Bean 标注在方法上给容器注册组件,默认是单实例的

Full 模式与 Lite 模式

  • 配置类组件之间无依赖关系用 Lite 模式加速容器启动过程,减少判断
  • 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用 Full 模式

proxyBeanMethods:代理 bean 的方法:

  • Full(proxyBeanMethods = true)保证每个 @Bean 方法被调用多少次返回的组件都是单实例的,Spring Boot 总会检查这个组件是否在容器中有,获取的都是之前注册容器中的单实例对象
  • Lite(proxyBeanMethods = false)每个 @Bean 方法被调用多少次返回的组件都是新创建的

组件依赖必须使用Full模式默认。其他默认是否Lite模式

配置类
@Configuration(proxyBeanMethods = true /*false*/) // 代理
public class MyConfig {
    @Bean
    public User user01(){
        User user = new User("张三", 17);
        user.setPet(petCat());
        return user;
    }
    @Bean("cat")
    public Pet petCat(){
        return new Pet("tom",78);
    }
}

Bean
public class User {
    private String username;
    private int age;
    private Pet pet;
}
  • 默认值为 true。为 false 时,每一次 new 出的对象都是不一样的。 在 User 类中嵌入 Pet 类,使用 User 类获取到的 Pet 类与直接获取 Pet 类不相等,而且使用两次 new 获取 Pet 类,值也不相等,即每次获取的不是同一个
  • 为 true 时,无论怎么样获取Pet类,无论获取多少次,都是相等的,即每次获取都是同一个。单实例
@SpringBootApplication
public class SpringinitApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringbootTestApplication.class, args);
  
        User user01 = (User) run.getBean("user01", User.class);
        System.out.println(user01);
        System.out.println("用户的宠物:" + user01.getPet());

        Pet pet = (Pet) run.getBean("cat",Pet.class);
        System.out.println(pet);

        System.out.println(user01.getPet() == pet);
    }
}

3.2 @Conditional

条件注解,需要某些 Bean 满足某种条件才加载

@Configuration(proxyBeanMethods = true) // 告诉SpringBoot这是一个配置类 == 配置文件
// @ConditionalOnBean(name = "tom") 存在名称为 tom 的 Bean 才去做某些事情
@ConditionalOnMissingBean(name = "tom")
public class MyConfig {
    /**
     * Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
     * @return
     */
    @Bean // 给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例
    public User user01(){
        User zhangsan = new User("zhangsan", 18);
        //user组件依赖了Pet组件
        zhangsan.setPet(tomcatPet());
        return zhangsan;
    }
    @Bean("tom22")
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }
}

在这里插入图片描述

注解 作用
@ConditionalOnProperty application.propertiesapplication.yml 中是否有满足条件的配置
@ConditionalOnBean Bean 已经存在应用上下文时才会加载
@ConditionalOnMissingBean Bean 不存在应用上下文时才会加载
@ConditionalOnClass 某个类存在于 classpath 中才加载
@ConditionalOnMissingClass 某个类不存在于 classpath 中才加载
@ConditionalOnExpression 当条件为 true 时才加载
@ConditionalOnSingleCandidate 只有指定类已存在于 BeanFactory 中,并且可以确定单个
@ConditionalOnResource 加载的 bean 依赖指定资源存在于 classpath
@ConditionalOnJndi 只有指定的资源通过 JNDI 加载后才加载 bean
@ConditionalOnJava 只有运行指定版本的 Java 才会加载 Bean
@ConditionalOnWebApplication 只有运行在 web 应用里才会加载这个 bean
@ConditionalOnNotWebApplication 只有运行在非 web 应用里才会加载这个 bean
@ConditionalOnCloudPlatform 只有运行在指定的云平台上才加载指定的 bean,CloudPlatform 是 org.springframework.boot.cloud 下一个 enum 类型的类

例:

@ConditionalOnPropertyapplication.propertiesapplication.yml 中是否有满足条件的配置

配置文件:

fan.property=true

使用:

// @ConditionalOnProperty(prefix = "fan", name = "property", havingValue = "true")
@ConditionalOnProperty(value = "fan.property", havingValue = "true")

3.2 @Import、@ImportResource

@Import:

// 给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名
@Import({User.class, DBHelper.class})
@SpringBootApplication
public class SpringbootTestApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringbootTestApplication.class, args);
        String[] users = run.getBeanNamesForType(User.class);
        for (String user : users) {
            System.out.println(user);
        }
    }
}

在这里插入图片描述
@ImportResource:引入自定义配置文件

@ImportResource("classpath:beans.xml")

3.3 @Bean

设置 Bean,给容器中添加组件,以方法名作为组件的 id,返回类型就是组件类型,返回值就是组件的容器中的实例。配置类里面使用 @Bean 标注在方法上给容器注册组件,默认是单实例的

@Bean
public User user01(){
	User user = new User("张三", 17);
	user.setPet(petCat());
	return user;
}

@Bean("cat")
public Pet petCat(){
	return new Pet("tom",78);
}
@SpringBootApplication
public class SpringbootTestApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringbootTestApplication.class, args);
        String[] beanDefinitionNames = run.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println(beanDefinitionName); // 打印输出所有组件名
        }
    }
}

3.4 配置绑定

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

@Component + @ConfigurationProperties

/**
 * 只有在容器中的组件,才会拥有SpringBoot提供的强大功能
 */
@Data
@Component // 注入容器
@ConfigurationProperties(prefix = "user")
public class User {
    private Integer id;
    private String name;
    private String gender;
    private String[] hobby;
}
user:
  id: 1
  name: 张三
  hobby: [篮球, 游泳]
@Controller
public class TestController {
    @Autowired
    private User user;

    @RequestMapping("/index")
    @ResponseBody
    public User index(){
        return user;
    }
}

@EnableConfigurationProperties + @ConfigurationProperties

@Data
@ConfigurationProperties(prefix = "user")
public class User {
    private Integer id;
    private String name;
    private String gender;
    private String[] hobby;
}
@Configuration
@EnableConfigurationProperties(User.class)
// 1、开启Car配置绑定功能
// 2、把这个Car这个组件自动注册到容器中
public class MyConfig {
}

3.5 配置绑定提示

自定义的类和配置文件绑定一般没有提示

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

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

3.6 @Value 与 @ ConfigurationProperties 对比

@Value 和 @ConfigurationProperties 注解都能读取配置文件中的属性值并绑定到 JavaBean 中。当我们只需要读取配置文件中的某一个配置时,可以通过 @Value 注解获取

  1. 使用位置不同
    @ConfigurationProperties:标注在 JavaBean 的类名上
    @Value:标注在 JavaBean 的属性上

  2. 功能不同
    @ConfigurationProperties:用于批量绑定配置文件中的配置
    @Value:只能一个一个的指定需要绑定的配置

  3. 松散绑定支持不同
    @ConfigurationProperties:支持松散绑定(松散语法),例如实体类 Person 中有一个属性为 lastName,那么配置文件中的属性名支持以下写法:

    • person.firstName
    • person.first-name
    • person.first_name
    • PERSON_FIRST_NAME

    @Vaule:不支持松散绑定

  4. 应用场景不同
    @Value 和 @ConfigurationProperties 两个注解之间,并没有明显的优劣之分,它们只是适合的应用场景不同而已。

    • 若只是获取配置文件中的某项值,则推荐使用 @Value 注解
    • 若专门编写了一个 JavaBean 来和配置文件进行映射,则建议使用 @ConfigurationProperties 注解

3.7 @PropertySource

如果将所有的配置都集中到 application.properties 或 application.yml 中,那么这个配置文件会十分的臃肿且难以维护,因此我们通常会将与 Spring Boot 无关的配置(例如自定义配置)提取出来,写在一个单独的配置文件中,并在对应的 JavaBean 上使用 @PropertySource 注解指向该配置文件
在这里插入图片描述

@PropertySource(value = "classpath:person.properties") //指向对应的配置文件
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
    private String lastName;
    private Integer age;
    private Boolean boss;
    private Date birth;
    private Map<String, Object> maps;
    private List<Object> lists;
    private Dog dog;
}

4. 基本配置

4.1 SpringMVC 的自动配置概览

Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)

The auto-configuration adds the following features on top of Spring’s defaults(添加如下功能):

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
    内容协商视图解析器和 BeanName 视图解析器
  • Support for serving static resources, including support for WebJars (covered later in this document)).
    静态资源(包括webjars)
  • Automatic registration of Converter, GenericConverter, and Formatter beans.
    自动注册 Converter,GenericConverter,Formatter
  • Support for HttpMessageConverters (covered later in this document).
    支持 HttpMessageConverters (配合内容协商理解原理)
  • Automatic registration of MessageCodesResolver (covered later in this document).
    自动注册 MessageCodesResolver (国际化用)
  • Static index.html support.
    静态 index.html 页支持
  • Custom Favicon support (covered later in this document).
    自定义 Favicon
  • 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

4.2 设置端口

http://localhost:xxx/aaa.png

server:
  port: xxxx     // 端口号

4.3 静态资源访问

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

它们的优先级顺序为:classpath:/META-INF/resources/ > classpath:/resources/ > classpath:/static/ > classpath:/public/

访问 : 当前项目根路径/ + 静态资源名。原理: 静态映射/

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

4.3.1 改变默认的静态资源路径

改变默认的静态资源路径后,原先的默认路径就失效了

spring:
  web:
    resources:
      static-locations: [classpath:/aaa/]
#      static-locations: classpath:/aaa/ classpath之后的不能加空格

4.3.2 静态资源访问前缀

设置后在访问的静态资源路径前必须加上 /xxx。http://localhost:8080/xxx/aaa.png

spring:
  mvc:
    static-path-pattern: /xxx/**         // 路径 http://localhost:8080/xxx/**

4.3.3 webjar

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

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

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

4.4 浏览器访问路径

设置后浏览器访问的路径前必须加上 /xxxhttp://localhost:8080/world/res/aaa.png
/访问路径/静态资源路径

server:
  servlet:
    context-path: /xxx                   // 路径 http://localhost:8080/xxx/

4.5 欢迎页支持

  • 静态资源路径下 index.html

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

4.6 自定义 Favicon

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

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

4.7 静态资源配置原理

  • Spring Boot 启动默认加载 xxxAutoConfiguration 类(自动配置类)
  • SpringMVC 功能的自动配置类 WebMvcAutoConfiguration,生效
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
    @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
    @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
    		ValidationAutoConfiguration.class })
    public class WebMvcAutoConfiguration {}
    
  • 给容器中配了什么
    	@Configuration(proxyBeanMethods = false)
    	@Import(EnableWebMvcConfiguration.class)
    	@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
    	@Order(0)
    	public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
    
  • 配置文件的相关属性和 xxx 进行了绑定。WebMvcProperties==spring.mvcResourceProperties==spring.resources

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

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

4.7.2 资源处理的默认规则

@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/**

  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;

4.7.3 欢迎页的处理规则

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

4.8 设置 Rest 响应方式

4.8.1 激活 Rest 响应方式

激活,设置为true
spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true

4.8.2 设置配置类,自定义值

@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
	HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
	hiddenHttpMethodFilter.setMethodParam("xxx");   // 自定义值
	return hiddenHttpMethodFilter;
}

4.8.3 HTML 设置表单提交响应方式,正常 GET 和 POST 提交不改变

<form action="/user" method="post">
<!--    <input name="_method" type="hidden" value="DELETE">-->   // 默认值为_method
    <input name="xxx" type="hidden" value="DELETE">              // 设置后自定义值xxx
    <input value="reset_delete" type="submit">
</form>

<form action="/user" method="post">
<!--    <input name="_method" type="hidden" value="PUT">-->      // 默认值为_method
    <input name="xxx" type="hidden" value="PUT">                 // 设置后自定义值xxx
    <input value="reset_put" type="submit">

4.8.4 RestController 类

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

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

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

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

4.9 设置 Bean 的值

YAML 文件,双引号 "张三" 和单引号 '张三' 与直接写 张三 一样,但是 \n 在单引号中会作为字符串输出,在双引号中会作为换行输出,双引号不会转义,单引号会转义

person:
  user-name: 张三
  boss: true
  birth: 2001/12/5
  age: 15
#  String[]
  interests: #  intersts: [篮球,足球]
    - 篮球
    - 足球
    - 17
  
#  List<String>
  animal: [猫,狗]
  
#  score:
#    english: 80
#    math: 70
#  Map<String, Object>
  score: {math:79,english:80}
  
#  Set<Integer>
  salarys:
    - 8888
    - 5555
  
  pet:
    name: 猫
    weight: 99
  
# Map<String, List<Pet>>
  allPets:
    sick:
      - {name: 狗, weight: 88.4}
      - name: 猫
        weight: 54.3
    health: [{name: 乌龟, weight: 55.3}, {name: 鱼, weight: 44.2}]

配置Bean(1)

Bean
@Component   // 需要加容器
@ConfigurationProperties(prefix = "person")
public class Person {
    private String userName;
    private Boolean boss;
    private Date birth;
    private Integer age;
    private Pet pet;
    private String[] interests;
    private List<String> animal;
    private Map<String, Object> score;
    private Set<Integer> salarys;
    private Map<String, List<Pet>> allPets;
}

配置类和Bean(2)

配置类
@Configuration
@EnableConfigurationProperties(Car.class) // 设置了Bean的class,则不需要加容器
public class MyConfig {
}

Bean
//@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {
    private String brand;
}
@Controller
public class TestController {
    @Autowired
    private User user;

    @RequestMapping("/index")
    @ResponseBody
    public User index(){
        return user;
    }
}

5. 请求参数处理、数据响应与内容协商

5.1 请求映射

5.1.1 REST 使用与原理

  • @xxxMapping

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

    • 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
    • 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
    • 核心Filter;HiddenHttpMethodFilter
      • 用法: 表单 method=post,隐藏域 _method=put
      • Spring Boot 中手动开启
    • 扩展:如何自定义 _method 这个名字
    @RequestMapping(value = "/user",method = RequestMethod.GET)
    public String getUser(){
    	return "GET-张三";
    }
    
    @RequestMapping(value = "/user",method = RequestMethod.POST)
    public String saveUser(){
    	return "POST-张三";
    }
    
    
    @RequestMapping(value = "/user",method = RequestMethod.PUT)
    public String putUser(){
    	return "PUT-张三";
    }
    
    @RequestMapping(value = "/user",method = RequestMethod.DELETE)
    public String deleteUser(){
    	return "DELETE-张三";
    }
    
    @Bean
    @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
    @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
    	return new OrderedHiddenHttpMethodFilter();
    }
    
    
    // 自定义filter
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
    	HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
    	methodFilter.setMethodParam("_m"); // 将_method属性自定义为_m
    	return methodFilter;
    }
    
  • REST 原理(表单提交要使用 REST 的时候)

    • 表单提交会带上 _method=PUT
    • 请求过来被 HiddenHttpMethodFilter 拦截
      • 请求是否正常,并且是 POST
        • 获取到 _method 的值
        • 兼容以下请求:PUT.DELETE.PATCH
        • 原生request(post),包装模式 requesWrapper 重写了 getMethod 方法,返回的是传入的值
        • 过滤器链放行的时候用 wrapper。以后的方法调用 getMethod 是调用 requesWrapper 的
  • REST 使用客户端工具
    如 Postman 直接发送 Put、DELETE 等方式请求,无需 Filter

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true   #开启页面表单的Rest功能

5.1.2 请求映射原理

在这里插入图片描述
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

在这里插入图片描述
RequestMappingHandlerMapping:保存了所有 @RequestMapping 和 handler 的映射规则
在这里插入图片描述
所有的请求映射都在 HandlerMapping 中

  • Spring Boot 自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到 index.html;
  • Spring Boot 自动配置了默认 的 RequestMappingHandlerMapping
  • 请求进来,挨个尝试所有的 HandlerMapping 看是否有请求信息。
    • 如果有就找到这个请求对应的 handler
    • 如果没有就是下一个 HandlerMapping
  • 需要一些自定义的映射处理,我们也可以自己给容器中放 HandlerMapping。自定义 HandlerMapping
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;
}

5.2 获取请求参数 Param 的值

5.2.1 @PathVariable

获取路径中 {xxx} 的值。http://localhost:8080/car/111/owner/zhangsan

@RequestMapping("car/{id}/owner/{username}")
public void getCar(@PathVariable("id") int id,
                   @PathVariable("username") String username,
                   @PathVariable Map<String,String> pv)

5.2.2 @RequestHeader

获取请求头的值

@RequestMapping("car/{id}/owner/{username}")
public void get(@RequestHeader("User-Agent") String userAgent,
                @RequestHeader Map<String,String> header){
	Map<String,Object> map = new HashMap<>();
	map.put("userAgent",userAgent);
	map.put("headers",header);
	return map;
}

5.2.3 @CookieValue

获取 Cookie 的值

@RequestMapping("car/{id}/owner/{username}")
public void get(@CookieValue("xxx") String cookie1, // xxx为cookie名
                @CookieValue("xxx") Cookie cookie){
	String s = cookie.getName() + cookie.getValue()
}

5.2.4 @RequestParam

获取传递参数的值。http://localhost:8080/car/111/owner/zhangsan?age=18&inters=sing&inters=play

@RequestMapping("car/{id}/owner/{username}")
public void get(@RequestParam("age") int age,
                @RequestParam("inters") List<String> inters,
                @RequestParam Map<String,String> param){
}

5.3 获取 Request 域中的值

5.3.1 跳转 forward:/xxx

@RequestMapping("/goto")
public String gotoPage(HttpServletRequest request){
    request.setAttribute("code",200);
    return "forward:/success";
}

@ResponseBody
@RequestMapping("/success")
public Map success(@RequestAttribute(value = "msg",required = false) String msg,
                   @RequestAttribute("code") int code){
  
}

5.3.2 @RequestAttribute

获取 request 请求域的值,也可通过 HttpServletRequest 直接获取 request,然后通过 request 获取值。设置 required = false,获取的值为非必须,即请求域中可以不存在该值

@RequestMapping("/success")
public Map success(@RequestAttribute(value = "msg",required = false) String msg,
                   @RequestAttribute("code") int code,
                   HttpServletRequest request){
  
    Object msg1 = request.getAttribute("msg");
    Object code1 = request.getAttribute("code");
}

5.3.3 request 请求域对象

在 request 请求域中,可以传递 Map、Model 和 request、response 对象,同时 Map 和 Model 对象都可以直接通过 request 在请求域中获取到。传递的 Map 和 Model 底层其实是一个对象

@RequestMapping("/goto")
public String gotoPage(HttpServletRequest request,
                       Map<String,Object> map,
                       Model model,
                       HttpServletResponse response){
    map.put("map","map");
    model.addAttribute("model","model");
    request.setAttribute("code",200);

    Cookie cookie = new Cookie("c1","v1");
    response.addCookie(cookie);

    return "forward:/success";
}

@ResponseBody
@RequestMapping("/success")
public Map success(HttpServletRequest request,
                  @CookieValue("c1") String c1){
    Object map = request.getAttribute("map");
    Object model = request.getAttribute("model");
    Object code = request.getAttribute("code");
}

5.4 矩阵变量

  1. 语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
  2. Spring Boot 默认是禁用了矩阵变量的功能
    手动开启:原理。对于路径的处理,UrlPathHelper 进行解析。
    removeSemicolonContent(移除分号内容) 支持矩阵变量的
  3. 矩阵变量必须有 URL 路径变量才能被解析

session.set(a,b) ---> jsessionid ---> cookie ---> 每次发送请求携带

url 重写:/abc;jsessionid=xxx 把 Cookie 的值使用矩阵变量进行传递

5.4.1 配置类开启矩阵变量

默认移除 url 地址中的 ; 符号,设置值为 false 则开启矩阵变量

  1. 实现 WebMvcConfigurer 接口,注入 Bean
    @Configuration
    public class MyConfig implements WebMvcConfigurer{
        @Bean
    	public WebMvcConfigurer webMvcConfigurer(){
    		return new WebMvcConfigurer() {
    			@Override
    			public void configurePathMatch(PathMatchConfigurer configurer) {
    				UrlPathHelper urlPathHelper = new UrlPathHelper();
    				urlPathHelper.setRemoveSemicolonContent(false);
    				configurer.setUrlPathHelper(urlPathHelper);
    			}
    		};
    	}
    }
    
  2. 实现 WebMvcConfigurer 接口,重写 configurePathMatch 方法
    @Configuration
    public class MyConfig implements WebMvcConfigurer{
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            UrlPathHelper urlPathHelper = new UrlPathHelper();
            urlPathHelper.setRemoveSemicolonContent(false);
            configurer.setUrlPathHelper(urlPathHelper);
        }
    }
    

5.4.2 获取矩阵变量

http://localhost:8080/cars/sell;low=34;brand=byd,audi,yd
http://localhost:8080/cars/sell;low=34;brand=byd;brand=audi;brand=yd

@RequestMapping("/cars/{path}") // 包括后面的分号内容也算路径
public Map<String,Object> carsSell(@MatrixVariable("low") Integer low,
                                   @MatrixVariable("brand") List<String> brand,
                                   @PathVariable("path") String path){
  
}

http://localhost:8080/boss/1;age=10/2;age=20

@RequestMapping("/boss/{bossId}/{empId}")
public Map<String,Object> boss(@MatrixVariable(value = "age",pathVar = "bossId") int bossAge,
                               @MatrixVariable(value = "age",pathVar = "empId") int empAge){
   
}

5.5 Servlet API

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

ServletRequestMethodArgumentResolver 以上的部分参数:

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

5.6 复杂参数

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

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

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

mavContainer.getModel(); 获取到值的
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.6 POJO 数据绑定

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

5.6.1 普通数据绑定

HTML 页面,提交后可自动绑定到 Person 类

<form action="/saveUser" method="post">
    姓名:<input name="username" value="zhangsan" /> <br>
    年龄:<input name="age" value="16"><br>
<!--    宠物姓名:<input name="pet.name" value="猫"><br>-->
<!--    宠物年龄:<input name="pet.age" value="18">-->
    <input name="pet" value="狗,3">
    <input type="submit">
</form>

Controller 类

Bean
public class Person {
    private String username;
    private int age;
    private Pet pet;
}

Controller
@ResponseBody
@RequestMapping("/saveUser")
public Person saveUser(Person person){
    return person;
}

5.6.2 自定义数据绑定

<input name="pet" value="狗,3">,配置类设置 convert,将传进来的值以 进行分割,第一个为姓名,第二个为年龄

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

5.7 参数处理原理

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

5.7.1 HandlerAdapter

在这里插入图片描述
0 - 支持方法上标注 @RequestMapping
1 - 支持函数式编程的
xxxxxx

5.7.2 执行目标方法

// Actually invoke the handler.
//DispatcherServlet -- doDispatch
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
mav = invokeHandlerMethod(request, response, handlerMethod); //执行目标方法

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

5.7.3 参数解析器 HandlerMethodArgumentResolver

  • 确定将要执行的目标方法的每一个参数的值是什么
  • SpringMVC 目标方法能写多少种参数类型。取决于参数解析器

在这里插入图片描述
在这里插入图片描述

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

5.6.4 返回值处理器

在这里插入图片描述

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

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

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

		Object[] args = new Object[parameters.length];
		for (int i = 0; i < parameters.length; i++) {
			MethodParameter parameter = parameters[i];
			parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
			args[i] = findProvidedArgument(parameter, providedArgs);
			if (args[i] != null) {
				continue;
			}
			if (!this.resolvers.supportsParameter(parameter)) {
				throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
			}
			try {
				args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
			}
			catch (Exception ex) {
				// Leave stack trace for later, exception may actually be resolved and handled...
				if (logger.isDebugEnabled()) {
					String exMsg = ex.getMessage();
					if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
						logger.debug(formatArgumentError(parameter, exMsg));
					}
				}
				throw ex;
			}
		}
		return args;
}

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

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

5.6.5.2 解析这个参数的值

调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可

5.6.5.3 自定义类型参数 封装 POJO

ServletModelAttributeMethodProcessor 这个参数处理器支持是否为简单类型

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

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

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

		Object attribute = null;
		BindingResult bindingResult = null;

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

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

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

		return attribute;
}
  • WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
  • WebDataBinder:Web 数据绑定器,将请求参数的值绑定到指定的 JavaBean 里面
  • WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到 JavaBean 中

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

@FunctionalInterfacepublic interface Converter<S, T>
在这里插入图片描述
在这里插入图片描述
可以给 WebDataBinder 里面自定义 Converterprivate static final class StringToNumber<T extends Number> implements Converter<String, T>

自定义 Converter

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

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

                    @Override
                    public Pet convert(String source) {
                        // 啊猫,3
                        if(!StringUtils.isEmpty(source)){
                            Pet pet = new Pet();
                            String[] split = source.split(",");
                            pet.setName(split[0]);
                            pet.setAge(Integer.parseInt(split[1]));
                            return pet;
                        }
                        return null;
                    }
                });
            }
        };
}

5.6.6 目标方法执行完成

将所有的数据都放在 ModelAndViewContainer;包含要去的页面地址 View。还包含 Model 数据
在这里插入图片描述

5.6.7 处理派发结果

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

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

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

		// Expose the model object as request attributes.
		exposeModelAsRequestAttributes(model, request);

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

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

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

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

		else {
			// Note: The forwarded resource is supposed to determine the content type itself.
			if (logger.isDebugEnabled()) {
				logger.debug("Forwarding to [" + getUrl() + "]");
			}
			rd.forward(request, response);
		}
}
暴露模型作为请求域属性
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
			HttpServletRequest request) throws Exception {

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

5.8 响应处理

5.8.1 响应 JSON

jackson.jar + @ResponseBody,给前端自动返回 JSON 数据

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
web 场景自动引入了 json 场景
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-json</artifactId>
    <version>2.6.4</version>
    <scope>compile</scope>
</dependency>

5.8.1.1 返回值解析器

在这里插入图片描述

try {
	this.returnValueHandlers.handleReturnValue(
	returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

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

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

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

5.8.1.2 返回值解析器原理

在这里插入图片描述

  1. 返回值处理器判断是否支持这种类型返回值 supportsReturnType
  2. 返回值处理器调用 handleReturnValue 进行处理
  3. RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的
    • 利用 MessageConverters 进行处理 将数据写为 JSON
      • 内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
      • 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据
      • SpringMVC 会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理
        • 得到 MappingJackson2HttpMessageConverter 可以将对象写为 JSON
        • 利用 MappingJackson2HttpMessageConverter 将对象转为 JSON 再写出去

在这里插入图片描述

5.8.1.3 SpringMVC 支持哪些返回值

ModelAndView
Model
View
ResponseEntity 
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;

5.8.1.4 HTTPMessageConverter 原理

  1. MessageConverter 规范
    在这里插入图片描述
    HttpMessageConverter:看是否支持将 此 Class 类型的对象,转为 MediaType 类型的数据
    例:Person 对象转为 JSON。或者 JSON 转为 Person
  2. 默认的 MessageConverter
    在这里插入图片描述
    0 - 只支持 Byte 类型的
    1 - String
    2 - String
    3 - Resource
    4 - ResourceRegion
    5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
    6 - MultiValueMap
    7 - true
    8 - true
    9 - 支持注解方式 XML 处理的
    最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的 jackson 的 objectMapper 转换的)
    在这里插入图片描述

5.8.2 内容协商

  1. 浏览器发送请求,在请求头中,Accept 传递可支持什么格式的返回内容,q 代表权重,优先级。*/* 表示所有格式都支持返回
    Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
  2. 服务器接收到数据后,判断浏览器要什么格式的返回内容,然后自己能够提供什么的格式返回内容。最优匹配。由 Accept 参数可知:xhtml 和 xml 优先级大于其他的返回格式,这个在请求头内。我们需要某种返回格式时需要指定参数,请求参数。可以使用 format 指定传递所需要的返回格式
  3. 开启基于请求参数的内容协商功能
    http://localhost:8080/test/person?format=json
spring:
    contentnegotiation:
      favor-parameter: true
<!-- 引入 xml 依赖 --> 
<dependency>
	<groupId>com.fasterxml.jackson.dataformat</groupId>
	<artifactId>jackson-dataformat-xml</artifactId>
</dependency>

5.8.3 内容协商原理

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

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

    • contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
    • 在这里插入图片描述
    • **HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型 **
    • 在这里插入图片描述
  3. 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)

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

  5. 客户端需要【application/xml】。服务端能力【10种、json、xml】
    在这里插入图片描述

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

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

在这里插入图片描述
导入了 jackson 处理 xml 的包,xml 的 converter 就会自动进来

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

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

5.8.4 自定义数据格式

5.8.4.1 先定义一个 Converter,设置自定义格式

public class XMessageConverter implements HttpMessageConverter<Person> {  // 返回Person格式
    @Override
    public boolean canRead(Class aClass, MediaType mediaType) {
        return false;
    }

    @Override
    public boolean canWrite(Class aClass, MediaType mediaType) {  // 返回
        return aClass.isAssignableFrom(Person.class);
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return MediaType.parseMediaTypes("application/x");   // 设置支持为x格式 即 ?format=x
    }

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

    @Override
    public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
        String data = person.getUsername() + ";" + person.getAge();
        OutputStream body = httpOutputMessage.getBody();
        body.write(data.getBytes());                           // 定义返回数据
    }
}

5.8.4.2 在配置类的 WebMvcConfigurer 中配置一个额外的MessageConverters

public class MyConfig {
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            @Override
            public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
                converters.add(new XMessageConverter());
            }
        };
    }
}

5.8.4.3 设置请求参数策略

将可传递的格式添加进 Map 集合里,添加了 json、xml 和自定义 x 格式,请求参数 json、xml和 x 格式则可正常返回。此时设置的是基于参数的策略 ParameterContentNegotiationStrategy
http://localhost:8080/test/person?format=x

public class MyConfig {
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            @Override
            public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
                converters.add(new XMessageConverter());
            }
    
            @Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                Map<String, MediaType> mediaTypes = new HashMap<>();
                mediaTypes.put("json",MediaType.APPLICATION_JSON);
                mediaTypes.put("xml",MediaType.APPLICATION_XML);
                mediaTypes.put("x",MediaType.parseMediaType("application/x"));

                ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(mediaTypes);
                configurer.strategies(Arrays.asList(strategy));
            }
        };
    }
}

5.8.4.4 添加请求头策略

由于没有添加基于请求头的策略,这样设置会覆盖请求头策略,请求头的返回格式会失效。此时直接请求,不加 format 设置请求参数,无论请求头是什么返回格式都为添加进 Map 集合的第一个元素格式,即为 json 格式。优先级。假如想要请求头生效,需要再加一个基于请求头的策略 HeaderContentNegotiationStrategy。同样的,还可以添加更多的策略。
http://localhost:8080/test/person

public class MyConfig {
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            @Override
            public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
                converters.add(new XMessageConverter());
            }
    
            @Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                Map<String, MediaType> mediaTypes = new HashMap<>();
                mediaTypes.put("json",MediaType.APPLICATION_JSON);
                mediaTypes.put("xml",MediaType.APPLICATION_XML);
                mediaTypes.put("x",MediaType.parseMediaType("application/x"));

                ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(mediaTypes);
                HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();
                configurer.strategies(Arrays.asList(strategy,headerStrategy));
            }
        };
    }
}

6. Thymeleaf 模板引擎

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

6.1 引入

6.1.1 引入 Starter

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

6.1.2 已自动配置

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

public static final String DEFAULT_SUFFIX = ".html";   //xxx.html

6.1.3 在 HTML 页面引入 Thymeleaf

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

6.2 使用语法

6.2.1 条件运算

<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a> 
<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="#{roles.manager}">User is a manager</p>
  <p th:case="*">User is some other thing</p>
</div>

6.2.2 迭代

<tr th:each="prod,status : ${prods}">
  			<td th:text="${status.count}">id</td>   // 计数
        <td th:text="${prod.name}">name</td>
        <td th:text="${prod.price}">price</td>
        <td th:text="${prod.inStock}? #{true} : #{false}">isStock</td>
</tr>

6.2.3 链接 @

前面加 / ,会自动将访问路径添加进来,只需要写后面的资源路径就行

<a th:href="@{/xxx}"> </a>
<form th:action="@{/xxx}"> </form>

携带参数在路径后面加上括号,如:@{/xxx(id = xxx)}

<a th:href="@{/xxx(id = ${user.id})}"> </a>

7. 拦截器

7.1 创建拦截器,实现 HandlerInter 接口

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

7.2 配置拦截器

同样的在 WebMvcConfigurer 里进行拦截器配置

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

7.3 过滤器 Filter 和拦截器 Inteceptor 详解及使用场景

7.3.1 过滤器和拦截器的区别

在这里插入图片描述

  1. 过滤器和拦截器触发时机不一样,过滤器是在请求进入容器后,但请求进入 Servlet 之前进行预处理的。 请求结束返回也是,是在 Servlet 处理完后,返回给前端之前
  2. 拦截器可以获取 IOC 容器中的各个 bean,而过滤器就不行,因为拦截器是 Spring 提供并管理的,Spring 的功能可以被拦截器使用,在拦截器里注入一个Service,可以调用业务逻辑。而过滤器是 JavaEE 标准,只需依赖 Servlet API,不需要依赖 Spring
  3. 过滤器的实现基于回调函数。而拦截器(代理模式)的实现基于反射
  4. Filter 是依赖于Servlet 容器,属于 Servlet 规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用
  5. Filter 的执行由 Servlet 容器回调完成,而拦截器通常通过动态代理(反射)的方式来执行
  6. Filter 的生命周期由 Servlet 容器管理,而拦截器则可以通过 IOC 容器来管理,因此可以通过注入等方式来获取其他 Bean 的实例,因此使用会更方便

过滤器和拦截器非常相似,但是它们有很大的区别

  • 最简单明了的区别就是过滤器可以修改 request,而拦截器不能
  • 过滤器需要在 Servlet 容器中实现,拦截器可以适用于 JavaEE,JavaSE 等各种环境
  • 拦截器可以调用 IOC 容器中的各种依赖,而过滤器不能
  • 过滤器只能在请求的前后使用,而拦截器可以详细到每个方法

在这里插入图片描述

  • 过滤器(Filter) :可以拿到原始的http请求,但是拿不到你请求的控制器和请求控制器中的方法的信息
  • 拦截器(Interceptor):可以拿到你请求的控制器和方法,却拿不到请求方法的参数
  • 切片(Aspect): 可以拿到方法的参数,但是却拿不到 HTTP 请求和响应的对象
    在这里插入图片描述

7.3.2 应用场景

拦截器是在 DispatcherServlet 这个 Servlet 中执行的,因此所有的请求最先进入 Filter,最后离开 Filter。其顺序如下:
Filter -> Interceptor.preHandle -> Handler -> Interceptor.postHandle -> Interceptor.afterCompletion -> Filter

拦截器应用场景

拦截器本质上是面向切面编程(AOP),符合横切关注点的功能都可以放在拦截器中来实现, 主要的应用场景包括:

  • 登录验证,判断用户是否登录。
  • 权限验证,判断用户是否有权限访问资源,如校验token
  • 日志记录,记录请求操作日志(用户ip,访问时间等),以便统计请求访问量。
  • 处理cookie、本地化、国际化、主题等。
  • 性能监控,监控请求处理时长等。
  • 通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现)

过滤器应用场景

  • 过滤敏感词汇(防止sql注入)
  • 设置字符编码
  • URL级别的权限访问控制
  • 压缩响应信息

在这里插入图片描述

8. 文件上传

8.1 HTML 页面

设置 type 为 file,多文件上传需要设置 multiple 属性,表单提交方式需要为 post ,并且需要加 enctype 属性

<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
  <div class="form-group">
		<label for="exampleInputFile">单文件上传</label>
		<input type="file" name="headerImg" id="exampleInputFile">
		<p class="help-block">Example block-level help text here.</p>
	</div>
	<div class="form-group">
		<label for="exampleInputFile">多文件上传</label>
		<input type="file" name="photos" multiple>
		<p class="help-block">Example block-level help text here.</p>
	</div>
</form>

8.2 设置文件大小限制

spring:
  servlet:
    multipart:
      max-file-size: 10MB       // 单个文件的最大大小
      max-request-size: 50MB    // 请求的总文件最大大小

8.3 Controller 类

使用 @RequestPart 接收文件,使用 transferTo 写出文件

@Controller
public class FormTestController {
    @PostMapping("/upload")
    public String upload(@RequestParam("email") String email,  // 参数值
                         @RequestPart("headerImg") MultipartFile headerImg, // 文件上传接收
                         @RequestPart("photos") MultipartFile[] photos){ // 多文件则数组接收
        if (!headerImg.isEmpty()){
            String originalFilename = headerImg.getOriginalFilename();  // 获取文件原始名
            try {
                headerImg.transferTo(new File("E:\\" + originalFilename)); // 服务器创建文件
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (photos.length > 0){
            for (MultipartFile photo : photos) {
                String originalFilename = photo.getOriginalFilename();
                try {
                    photo.transferTo(new File("E:\\" + originalFilename));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return "main";
    }
}

9. 异常处理

默认规则

  • 默认情况下,Spring Boot 提供 /error 处理所有错误的映射
  • 对于机器客户端,它将生成 JSON 响应,其中包含错误,HTTP 状态和异常消息的详细信息。对于浏览器客户端,响应一个 “whitelabel” 错误视图,以 HTML 格式呈现相同的数据
    在这里插入图片描述
  • 要对其进行自定义,添加 View 解析为 error
  • 要完全替换默认行为,可以实现 ErrorController 并注册该类型的 Bean 定义,或添加 ErrorAttributes 类型的组件以使用现有机制但替换其内容
  • error/ 下的 4xx,5xx 页面会被自动解析

9.1 自定义异常页面

  • 自定义错误页
    error/404.html、error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
  • @ControllerAdvice + @ExceptionHandler 处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的
  • @ResponseStatus + 自定义异常 ;底层是 ResponseStatusExceptionResolver ,把 @ResponseStatus 注解的信息底层调用 response.sendError(statusCode, resolvedReason);Tomcat 发送的 /error
  • Spring 底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常
    response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());

可以通过以下 3 种方式定制 Spring Boot 错误页面:

  • 自定义 error.html
  • 自定义动态错误页面
  • 自定义静态错误页面

9.2 界面优先级

在 templates 文件夹或静态资源文件夹下,创建一个 error 文件夹,存放错误界面,会自动解析,跳转到自定义的错误界面
在这里插入图片描述
可以直接在模板引擎文件夹(/resources/templates)下创建 error.html ,覆盖 Spring Boot 默认的错误视图页面(Whitelabel Error Page)
在这里插入图片描述
上述 5 种方式均可以定制 Spring Boot 错误页面,且它们的优先级顺序为:

  1. 自定义动态错误页面目录(精确匹配404.html)
  2. 自定义静态错误页面目录(精确匹配404.html)
  3. 自定义动态错误页面目录(模糊匹配4xx.html)
  4. 自定义静态错误页面目录(模糊匹配404.html)
  5. 自定义 error.html

9.3 页面错误信息

  1. message:异常信息
  2. status:状态码
  3. error:错误名称
  4. path:错误路径
  5. timestamp:时间戳

9.4 自定义全局异常处理

创建一个全局异常处理类,加上 @ControllerAdvice 注解,增强。同时加上 @ExceptionHandler 注解。会跳转到返回的页面,返回异常属性要放入请求域中

int num = 1/0;  // 空指针

// 处理整个web controller 的异常
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler({ArithmeticException.class,NullPointerException.class}) // 处理全局异常
    public String handleArithExcepyion(){
        return "login"; // 视图地址
    }
}

9.5 自定义异常类

加上 @ResponseStatus 注解,继承 RuntimeException ,运行时异常。手动抛出异常

if (list.size() > 3){   // 越界
	throw new UserTooManyException();
}

@ResponseStatus(value = HttpStatus.FORBIDDEN,reason = "用户数量太多")
public class UserTooManyException extends RuntimeException{    // 越界异常
    public UserTooManyException(){
    }
    public UserTooManyException(String message){
        super(message);
    }
}

会跳转到 /error 目录下的异常页面,然后输出异常信息

<h1 th:text="${message}"></h1>
<h1 th:text="${error}"></h1>
<h1 th:text="${status}"></h1>

9.6 自定义异常解析器

设置最高优先级后,所有的异常全部都会被自定义的异常解析器解析,所有的异常都会变成自定义异常解析器的异常。相当于全局异常处理规则。上面定义的所有异常都失效,全成为自定义异常解析器定义的异常。

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

9.7 定制错误数据

自定义错误属性处理工具

@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
        // 添加自定义的错误数据
        errorAttributes.put("company", "www.biancheng.net");
        // 获取 MyExceptionHandler 传入 request 域中的错误数据
        Map ext = (Map) webRequest.getAttribute("ext", 0);
        errorAttributes.put("ext", ext);
        return errorAttributes;
    }
}

10. Web 原生组件注入

10.1 原生 API

10.1.1 Servlet

创建 Servlet ,在启动类加上 @ServletComponentScan 注解,配置包扫描

@ServletComponentScan(basePackages = "fan")
@SpringBootApplication
public class AdminmanagerApplication {
    public static void main(String[] args) {
        SpringApplication.run(AdminmanagerApplication.class, args);
    }
}

直接访问即可响应,http://localhost:8081/MyServlet

@WebServlet(name = "MyServlet", value = "/MyServlet")
// @WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request,response);
    }
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.getWriter().write("6666");
    }
}

10.1.2 Filter 和 Listener

直接创建原生组件即可使用

  • @ServletComponentScan(basePackages = "com.atguigu.admin") :指定原生Servlet组件都放在那里
  • @WebServlet(urlPatterns = "/my"):效果:直接响应,没有经过 Spring 的拦截器

在这里插入图片描述

  • @WebFilter(urlPatterns={"/css/*","/images/*"})
  • @WebListener
@WebFilter(filterName = "MyFilter", urlPatterns = "/images/*")
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig config) throws ServletException {
        System.out.println("MyFilter初始化完成");
    }
    @Override
    public void destroy() {
        System.out.println("MyFilter销毁");
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        System.out.println("MyFilter工作");
        chain.doFilter(request, response);
    }
}
@WebListener
public class MyListener implements ServletContextListener, HttpSessionListener, HttpSessionAttributeListener {
    public MyListener() {
    }
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        /* This method is called when the servlet context is initialized(when the Web application is deployed). */
    }
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        /* This method is called when the servlet Context is undeployed or Application Server shuts down. */
    }
}

10.2 Spring Boot 的 RegistrationBean

创建一个配置类,注入 RegistrationBean 的 Bean。不使用 @WebServlet 等注解,在 RegistrationBean 里注入

10.2.1 Servlet

@Configuration
public class MyRegisterConfig {
    @Bean
    public ServletRegistrationBean myServlet(){
        MyServlet myServlet = new MyServlet();      // 创建的Servlet
        return new ServletRegistrationBean(myServlet,"/myServlet","/my");  // servlet多路径
    }
}

10.2.2 Filter

@Configuration
public class MyRegisterConfig {
     @Bean
    public ServletRegistrationBean myServlet(){
        MyServlet myServlet = new MyServlet();      // 创建的Servlet
        return new ServletRegistrationBean(myServlet,"/myServlet","/my");  // servlet多路径
    }
    @Bean
    public FilterRegistrationBean myFilter(){
        MyFilter myFilter = new MyFilter();         // 创建的Filter
//        return new FilterRegistrationBean(myFilter,myServlet());  // 直接配置过滤servlet
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*")); // 配置自定义路径
        return filterRegistrationBean;
    }
}

10.2.3 Listener

@Configuration
public class MyRegisterConfig {
    @Bean
    public ServletListenerRegistrationBean myListener(){
        MyListener myListener = new MyListener();   // 创建的Listener
        return new ServletListenerRegistrationBean(myListener);
    }
}

10.3 切换 Web 服务器

  1. 排除 Web 的 Tomcat 服务器
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
  2. 引入其他服务器
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>
    

10.4 定制 Servlet 容器

  1. 实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
    把配置文件的值和 ServletWebServerFactory 进行绑定
    xxxxxCustomizer:定制化器,可以改变 xxxx 的默认规则
    @Component
    public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
        @Override
        public void customize(ConfigurableServletWebServerFactory server) {
            server.setPort(9000);
        }
    
    }
    
  2. 修改配置文件 server.xxx
  3. 直接自定义 ConfigurableServletWebServerFactory

10.5 定制化原理

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

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

11. JDBC 导入与 Druid 数据源

11.1 JDBC 导入

11.1.1 导入 Starter

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

数据源的自动配置-HikariDataSource
在这里插入图片描述
自动配置的类:

  • 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") 来修改JdbcTemplate
    • @Bean @Primary JdbcTemplate;容器中有这个组件
  • JndiDataSourceAutoConfiguration: jndi 的自动配置

  • XADataSourceAutoConfiguration: 分布式事务相关的

11.1.2 导入数据库驱动

Spring Boot自带版本仲裁,因此可以不用写版本信息,目前默认版本为 8.0.26 。同时 MySQL 驱动高版本兼容低版本。即使电脑版本为 MySQL 5 也可使用 MySQL 8 的驱动。修改版本直接加版本信息即可

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

修改配置项,url 、username 、 password 、driver-class-name 属性

spring:
  datasource:
    url: jdbc:mysql:///xxx
    username: root
    password: xxx
    driver-class-name: com.mysql.jdbc.Driver # 会自动适配高版本写法
#    driver-class-name: com.mysql.cj.jdbc.Driver    // 高版本写法

11.2 JdbcTemplate

  1. 可修改配置项
    spring:
      jdbc:
        template:
          query-timeout: 3    // 单位为秒
    
  2. 自动配置
    已经自动配置好 JdbcTemplate ,自动注入使用即可
    @Autowired
    JdbcTemplate jdbcTemplate;
    
    @ResponseBody
    @GetMapping("/sql")
    public String sql(){
    	String sql = "select count(*) from user";
    	Integer integer = jdbcTemplate.queryForObject(sql, Integer.class);
    	return integer.toString();
    }
    

11.3 Druid 数据源

https://github.com/alibaba/druid/wiki/常见问题

11.3.1 自定义方式

11.3.1.1 引入 Druid 依赖

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

11.3.1.2 创建配置类

@Configuration
public class MyDataSourceConfig {
    @Bean
    public DataSource dataSource() throws SQLException {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl();
        druidDataSource.setUsername();
        druidDataSource.setPassword();
        return druidDataSource;
    }
}

需要设置 url、username、password 等属性,可以使用配置文件来进行配置,由于与 JDBC 配置属性相同,可以使用 @ConfigurationProperties 注解引入 JDBC 的配置信息

@Configuration
public class MyDataSourceConfig {
    @ConfigurationProperties("spring.datasource")
    @Bean
    public DataSource dataSource() throws SQLException {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }
}

可使用 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>

11.3.1.3 监控统计功能

http://localhost:8080/druid

  1. 开启监控统计和防火墙

    @Configuration
    public class MyDataSourceConfig {
        @ConfigurationProperties("spring.datasource")
        @Bean
        public DataSource dataSource() throws SQLException {
            DruidDataSource druidDataSource = new DruidDataSource();
    //        druidDataSource.setFilters("stat,wall");    // 开启监控统计、防火墙
            return druidDataSource;
        }
    }
    
  2. 同样可在配置文件中配置 filters 属性

    spring:
      datasource:
        url: jdbc:mysql:///db1
        username: root
        password: fan223
        driver-class-name: com.mysql.cj.jdbc.Driver
    
        filters: stat,wall   // 配置监控统计、防火墙
    
  3. StatViewServlet
    提供监控信息展示的 HTML 页面、提供监控信息的 JSON API
    可以在 StatViewServlet 中设置登录用户名和密码,设置后输入用户名和密码才可以进行监控

    @Configuration
    public class MyDataSourceConfig {
        @Bean
        public ServletRegistrationBean statViewServlet(){
            StatViewServlet statViewServlet = new StatViewServlet();
            ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet,"/druid/*");
            registrationBean.addInitParameter("loginUsername","admin");  // 登录用户名
            registrationBean.addInitParameter("loginPassword","123");	 // 登录密码
            return registrationBean;
        }
    }
    
    <servlet>
    	<servlet-name>DruidStatView</servlet-name>
    	<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
    </servlet>
    <servlet-mapping>
    	<servlet-name>DruidStatView</servlet-name>
    	<url-pattern>/druid/*</url-pattern>
    </servlet-mapping>
    
  4. StatFilter
    用于统计监控信息,如 SQL 监控、URL 监控

    @Configuration
    public class MyDataSourceConfig {
        @Bean
        public FilterRegistrationBean webStatFilter(){
            WebStatFilter webStatFilter = new WebStatFilter();
            FilterRegistrationBean<WebStatFilter> webStatFilterFilterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
            webStatFilterFilterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));    // 拦截路径
            webStatFilterFilterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); // 释放路径
            return webStatFilterFilterRegistrationBean;
        }
    }
    
    需要给数据源中配置如下属性;可以允许多个filter,多个用,分割;如:
    
    <property name="filters" value="stat,slf4j" />
    
  5. 系统中所有 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 |

  6. 慢 SQL 记录配置:

    <bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter">
        <property name="slowSqlMillis" value="10000" />
        <property name="logSlowSql" value="true" />
    </bean>
    
    使用 slowSqlMillis 定义慢SQL的时长
    

11.3.2 使用官方 starter 方式

11.3.2.1 引入 druid-starter

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

11.3.2.2 自动配置

  1. 扩展配置项 spring.datasource.druid
  2. DruidSpringAopConfiguration.class, 监控SpringBean的;配置项: spring.datasource.druid.aop-patterns
  3. DruidStatViewServletConfiguration.class, 监控页的配置: spring.datasource.druid.stat-view-servlet;默认开启
  4. DruidWebStatFilterConfiguration.class, web监控配置; spring.datasource.druid.web-stat-filter;默认开启
  5. 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";

11.3.2.3 配置项

spring:
  datasource:
    druid:
      aop-patterns: fan.*  #监控SpringBean,包
      filters: stat,wall     # 底层开启功能,stat(sql监控),wall(防火墙)

      stat-view-servlet:   # 配置监控页功能
        enabled: true      // 开启
        login-username: admin
        login-password: xxx
        resetEnable: false

      web-stat-filter:  # 监控web
        enabled: true
        urlPattern: /*
        exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'

      filter:
        stat:    # 对上面filters里面的stat的详细配置
          slow-sql-millis: 1000
          logSlowSql: true
          enabled: true
        wall:    # 对上面filters里面的wall的详细配置
          enabled: true
          config:
            drop-table-allow: false

12. 整合 Mybatis 和 Mybatis-Plus

12.1 整合 Mybatis

最佳实战:

  1. 引入 mybatis-starter
  2. 配置 application.yaml 中,指定 mapper-location 位置即可
  3. 编写 Mapper 接口并标注 @Mapper 注解 ,或在启动类指定 MapperSacn 包扫描
  4. 简单方法直接注解方式
  5. 复杂方法编写mapper.xml进行绑定映射
  6. @MapperScan("fan.mapper") 简化,其他的接口就可以不用标注 @Mapper 注解

12.1.1 引入 Starter

https://github.com/mybatis
引入 Mybatis 的时候其实已经引了 JDBC 核心包,之前的 JDBC 核心包就不用引了

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

12.1.2 配置模式

  1. 全局配置文件
  2. SqlSessionFactory:自动配置好了
  3. SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession
  4. @Import(AutoConfiguredMapperScannerRegistrar.class)
  5. Mapper: 只要我们写的操作 MyBatis 的接口标准了 @Mapper 就会被自动扫描进来

12.1.3 配置项

  1. 可创建 Mybatis 映射文件 Mapper.xml
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="fan.UserMapper">
        <select id="getUser" resultType="fan.User">
            select * from  user where  id=#{id}
        </select>
    </mapper>
    
  2. Yaml 配置
    可以不写全局配置文件,所有全局配置文件的配置都放在 configuration 配置项中即可,就是相当于改 mybatis 全局配置文件中的值,因此两者不能共存
    mybatis:
    #  config-location: classpath:mybatis/mybatis-config.xml      #全局配置文件位置
      mapper-locations: classpath:mybatis/mapper/*.xml            #sql映射文件位置
      configuration:
        map-underscore-to-camel-case: true
    
  3. 注解模式
    无需编写 Mapper.xml 文件
    public interface TestMapper extends BaseMapper {
        @Select("select * from test where id = #{id}")
        public Test getTest(int id);
    
        @Insert("insert into test values(null,#{name},#{age},#{gender})")
        @Options(useGeneratedKeys = true,keyProperty = "id")
        public void insert(User user);
    }
    

12.2 整合 Mybatis-Plus

12.2.1 引入 Starter

引入 Mybatis-Plus 的同时已经引入了 Mybatis 和JDBC 的核心包,因此之前的两个核心包可以不用引入

<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
	<version>3.4.1</version>
</dependency>

12.2.2 自动配置

  1. MybatisPlusAutoConfiguration 配置类,MybatisPlusProperties 配置项绑定。mybatis-plus:xxx 就是对 mybatis-plus 的定制
  2. SqlSessionFactory 自动配置好。底层是容器中默认的数据源
  3. mapperLocations 自动配置好的。有默认值。classpath:/mapper/**/.xml;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下
  4. 容器中也自动配置好了 SqlSessionTemplate
  5. @Mapper 标注的接口也会被自动扫描;建议直接 @MapperScan("fan.mapper") 批量扫描就行

优点:
只需要我们的 Mapper 继承 BaseMapper 就可以拥有 CRUD 能力,无需编写 Mapper.xml 文件

12.3 整合 Mybatis-Plus 的 CRUD

  1. 创建Bean
    一般来说 Bean 的名字与数据库表名对应,假如数据库表名修改了,可以使用 @TableName 标注数据库表名

    // @TableName("xxx") // 对应数据库表名
    public class User {
        private int id;
        private String name;
        private int age;
        private String gender;
    }
    
  2. 创建 Mapper 接口
    继承 BaseMapper,标注 Bean

    public interface UserMapper extends BaseMapper<User> {
    }
    
  3. 创建 Service
    继承 ServiceImpl ,标注 Mapper 接口 和 Bean

    public interface UserService extends IService<User> {
    
    }
    
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
    
    }
    
  4. 创建 Controller
    可直接使用 UserService 接口,调用 Mybatis-Plus 自带的 SQL 方法,进行 CRUD 操作
    list() 方法,返回 List 集合,查询到的所有数据
    page() 方法,返回分页数据

    1. records:数据集合
    2. total:数据条数
    3. pages:总页数
    4. current:当前页码
    @Autowired
    UserService userService;
    
    @GetMapping("/editable_table")
    public String editable_table(@RequestParam(value = "currentPage",defaultValue = "1") int currentPage, Model model){
            List<User> list = UserService.list();
    //        model.addAttribute("users",list);
    
            Page<User> userPage = new Page<>(currentPage,5);    // 创建一个 BeanPage 对象,传入当前页码和每页显示数据数
            Page<User> page = userService.page(userPage, null); // 调用 page 方法,传入 BeanPage 对象和条件
            if (currentPage < 1){
                userPage.setCurrent(1);
            }else if (currentPage > page.getPages()){
                userPage.setCurrent(page.getPages());
            }
            page = userService.page(userPage,null);
            model.addAttribute("page",page);
            return "table/editable_table";
    }
    
  5. HTML 页面

    <tr class="" th:each="user,stats : ${page.records}">
    		<td th:text="${stats.count}">Jonathan</td>    // 可以 .属性名
    		<td th:text="${user.id}">Jonathan</td>
    		<td th:text="${user.getName()}">Smith</td>		// 也可以 get()方法
    		<td>[[${user.getAge}]]</td>
    		<td>[[${user.gender}]]</td>
    		<td>
    				<a th:href="@{/userDel/{id}(id = ${user.id},currentPage = ${page.current})}" class="btn btn-danger btn-sm">删除</a>
    		</td>
    </tr>
    
    <ul>
    		<li class="prev"><a th:href="@{/editable_table(currentPage = ${page.current} - 1)}">← Prev</a></li>
    		<li th:class="${num == page.current} ? 'active' : ''" th:each="num : ${#numbers.sequence(1,page.pages)}">
    				<a th:href="@{/editable_table(currentPage = ${num})}">[[${num}]]</a>
    		</li>
    		<li class="next"><a th:href="@{/editable_table(currentPage = ${page.current} + 1)}">Next → </a></li>
    </ul>
    

13. 整合 NoSQL(Redis)

13.1 引入 Starter

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

13.2 自动配置

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

13.3 配置项

spring:
  redis:
    host: 124.222.118.90 # Redis服务器地址
    port: 6379 # Redis服务器连接端口
    client-type: lettuce # 可以选择客户端类型,默认为 lettuce
    lettuce:
      pool:
        max-active: 10 # 连接池最大连接数(使用负值表示没有限制)
        max-wait: -1 # 最大阻塞等待时间(负数表示没限制)
        max-idle: 5 # 连接池中的最大空闲连接
        min-idle: 0 # 连接池中的最小空闲连接
    jedis:
      pool:
        max-active: 10
        max-wait: 10
    password: xxx # 密码
    database: 0 # Redis数据库索引(默认为0)
    connect-timeout: 1800000 # 连接超时时间(毫秒)

13.4 切换至 Jedis

默认使用的是 Lettuce

  1. 导入 Jedis 依赖
    <dependency>
    	<groupId>redis.clients</groupId>
    	<artifactId>jedis</artifactId>
    </dependency>
    
  2. 更改 Client-type 属性为 Jedis
    spring:
      redis:
        port: 6379                  // 端口号
        host: localhost							// 主机
        client-type: jedis				// 客户端类型,Lettuce和Jedis
    

13.5 Controller

首先注入 RedisTemplate,然后使用 redisTemplate 的 opsForValue() 方法得到一个对象。使用该对象来对Redis 进行操纵
使用该对象的 increment(xxx) 方法,表示 xxx 的值自动加 1

@Controller
public class IndexController {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @GetMapping("/index.html")
    public String index(Model model){
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        operations.set("name","张三");
        String name = operations.get("name");
        System.out.println(name);
        return "index";
    }
}

14. Junit5 单元测试

14.1 引入 Starter

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

14.2 选择引入 JUnit Vintage 测试引擎

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

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

14.3 JUnit5 常用注解

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

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

14.4 断言(assertions)

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

  • 检查业务逻辑返回的数据是否合理
  • 所有的测试运行结束以后,会有一个详细的测试报告

14.1.1 简单断言

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 True
assertFalse 判断给定的布尔值是否为 False
assertNull 判断给定的对象引用是否为 NULL
assertNotNull 判断给定的对象引用是否不为 NULL
@Test
@DisplayName("simple assertion")
public void simple() {
     assertEquals(3, 1 + 2, "simple math");
     assertNotEquals(3, 1 + 1);

     assertNotSame(new Object(), new Object());
     Object obj = new Object();
     assertSame(obj, obj);

     assertFalse(1 > 2);
     assertTrue(1 < 2);

     assertNull(null);
     assertNotNull(new Object());
}

14.1.2 数组断言

@Test
@DisplayName("array assertion")
public void array() {
    assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}

14.1.3 组合断言

@Test
@DisplayName("assert all")
public void all() {
	assertAll("Math",
		() -> assertEquals(2, 1 + 1,"number"),
		() -> assertTrue(1 > 0)
	);
}

14.1.4 异常断言

@Test
@DisplayName("异常测试")
public void exceptionTest() {
    ArithmeticException exception = Assertions.assertThrows(
		//扔出断言异常
		ArithmeticException.class, () -> System.out.println(1 % 0));
}

14.1.5 超时断言

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

14.1.5 快速失败

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

@Test
@DisplayName("fail")
public void shouldFail() {
    if(1 == 1){
		fail("This should fail");
    }
}

14.5 前置条件(assumptions)

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

@DisplayName("前置条件")
public class AssumptionsTest {
	private final String environment = "DEV";
 
	@Test
	@DisplayName("simple")
 	public void simpleAssume() {
		assumeTrue(Objects.equals(this.environment, "DEV"));
		assumeFalse(() -> Objects.equals(this.environment, "PROD"));
	}

	@Test
	@DisplayName("assume then do")
	public void assumeThenDo() {
  	  assumingThat(
   	    Objects.equals(this.environment, "DEV"),
			() -> System.out.println("In DEV")
		);
	}
}

assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止
assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止

14.6 嵌套测试

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

@DisplayName("A stack")
class TestingAStackDemo {
    Stack<Object> stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }
    @Nested
    @DisplayName("when new")
    class WhenNew {
        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }
        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }
        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, stack::pop);
        }
        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, stack::peek);
        }
        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {
            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }
            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }
            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }
            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

14.7 参数化测试

参数化测试是 JUnit5 很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。标注 @ParameterizedTest 注解指定这是一参数化测试类,利用 @ValueSource 等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

  1. @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
  2. @NullSource: 表示为参数化测试提供一个 NULL 的入参
  3. @EnumSource: 表示为参数化测试提供一个枚举入参
  4. @CsvFileSource:表示读取指定 CSV 文件内容作为参数化测试入参
  5. @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
    System.out.println(string);
    Assertions.assertTrue(StringUtils.isNotBlank(string));
}

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

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

15. 指标监控

15.1 SpringBoot Actuator

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

15.1.1 引入 Starter

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

15.1.2 配置项

暴露规则

支持的暴露方式:

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

在这里插入图片描述

开启方式

默认所有的 Endpoint 除了 shutdown 都是开启的。需要开启或者禁用某个 Endpoint。配置模式为 management.endpoint.<endpointName>.enabled = true

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

或者禁用所有的 Endpoint 然后手动开启指定的 Endpoint ,设置相关端点属性

management:
  endpoints:
    enabled-by-default: false
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always  #总是显示详细信息。可显示每个模块的状态信息
      enabled: true
    info:
      enabled: true
    beans:
      enabled: true
    metrics:
      enabled: true

15.1.3 访问

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

15.2 Actuator Endpoint

常用 Endpoint:

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

在这里插入图片描述
如果应用程序是Web应用程序(Spring MVC,Spring WebFlux 或 Jersey),则可以使用以下附加端点:
在这里插入图片描述

15.2.1 Health Endpoint

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

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

定制 Health

  1. 继承抽象类
    @Component
    public class MyComHealthIndicator extends AbstractHealthIndicator {
        @Override
        protected void doHealthCheck(Health.Builder builder) throws Exception {
            Map<String,Object> map = new HashMap<>();
    
            if (1 == 1){
                builder.up();
                map.put("count",1);
                map.put("ms",1000);
            }else {
                builder.status(Status.OUT_OF_SERVICE);
                map.put("err","连接超时");
            }
            builder.withDetail("code",100)
                	.withDetails(map);
        }
    }
    
  2. 实现接口
    @Component
    public class MyHealthIndicator implements HealthIndicator {
        @Override
        public Health health() {
            int errorCode = check(); // perform some specific health check
            if (errorCode != 0) {
                return Health.down().withDetail("Error Code", errorCode).build();
            }
            return Health.up().build();
        }
    }
    // 构建Health
    Health build = Health.down()
                    .withDetail("msg", "error service")
                    .withDetail("code", "500")
                    .withException(new RuntimeException())
                    .build();
    

15.2.2 Metrics Endpoint

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

  • 通过Metrics对接多种监控系统
  • 简化核心Metrics开发
  • 添加自定义 Metrics 或者扩展已有 Metrics

定制Metrics:

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

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

15.2.3 定制 info 信息

  1. 编写配置文件
    info:
      appName: boot-admin
      version: 2.0.1
      mavenProjectName: @project.artifactId@  #使用@@可以获取maven的pom文件值
      mavenProjectVersion: @project.version@
    
  2. 编写 InfoContributor
    @Component
    public class ExampleInfoContributor implements InfoContributor {
        @Override
        public void contribute(Info.Builder builder) {
            builder.withDetail("ms","你好")
                    .withDetails(Collections.singletonMap("world","world"));
        }
    }
    

15.2.4 定制 Endpoint

@Component
@Endpoint(id = "myService")
public class MyServiceEndpoint {
    @ReadOperation
    public Map getDockerInfo(){
        return Collections.singletonMap("dockerInto","dockerInfo start");
    }
    @WriteOperation
    public void stopDockerInfo(){
        System.out.println("docker stopped");
    }
}

15.3 可视化

https://github.com/codecentric/spring-boot-admin
创建一个 springboot server 项目,用作服务器指标监控 springboot client 客户端

15.3.1 Server

  1. 引入 Starter
    <dependency>
    	<groupId>de.codecentric</groupId>
    	<artifactId>spring-boot-admin-starter-server</artifactId>
    	<version>2.3.1</version>
    </dependency>
    
  2. 添加 @EnableAdminServer 注解
    @EnableAdminServer
    @SpringBootApplication
    public class AdminserverApplication {
        public static void main(String[] args) {
            SpringApplication.run(AdminserverApplication.class, args);
        }
    }
    
  3. 设置端口
    server:
    	port: 8888
    

15.3.2 Client

spring:
  boot:
    admin:
      client:
        url: http://localhost:8888  // 使用主机名注册
        instance:
          prefer-ip: true  # 使用ip注册进来
	application:
		name: adminmanager

16. 外部化配置和默认配置文件

16.1 外部化配置文件

除了默认配置文件,Spring Boot 还可以加载一些位于项目外部的配置文件。可以通过如下 2 个参数,指定外部配置文件的路径:

  • spring.config.location
    可以先将 Spring Boot 项目打包成 JAR 文件,然后在命令行启动命令中,使用命令行参数 --spring.config.location,指定外部配置文件的路径。
    java -jar {JAR} --spring.config.location={外部配置文件全路径}
    使用该参数指定配置文件后,会使项目默认配置文件(application.properties 或 application.yml )失效,Spring Boot 将只加载指定的外部配置文件
  • spring.config.additional-location
    java -jar {JAR} --spring.config.additional-location={外部配置文件全路径}
    --spring.config.location 不同,--spring.config.additional-location 不会使项目默认的配置文件失效,使用该命令行参数添加的外部配置文件会与项目默认的配置文件共同生效,形成互补配置,且其优先级是最高的,比所有默认配置文件的优先级都高

16.1.1 配置外部配置文件的优先级

Maven 对项目进行打包时,位于项目根目录下的配置文件是无法被打包进项目的 JAR 包的,因此位于根目录下的默认配置文件无法在 JAR 中生效, 即该项目将只加载指定的外部配置文件和项目类路径(classpath)下的默认配置文件,它们的加载优先级顺序为:

  1. spring.config.additional-location 指定的外部配置文件 my-application.yml
  2. classpath:/config/application.yml
  3. classpath:/application.yml

16.1.2 配置虚拟机参数

将 Spring Boot 项目打包后,然后在命令行启动命令中添加 spring.config.additional-location 参数指定外部配置文件,会导致项目根目录下的配置文件无法被加载,可以通过以下 3 种方式解决这个问题:

  • 在 IDEA 的运行配置(Run/Debug Configuration)中,添加虚拟机参数 -Dspring.config.additional-location=D:\myConfig\my-application.yml,指定外部配置文件
  • 在 IDEA 的运行配置(Run/Debug Configuration)中,添加程序运行参数 --spring.config.additional-location=D:\myConfig\my-application.yml,指定外部配置文件;
  • 在主启动类中调用 System.setProperty()方法添加系统属性 spring.config.additional-location,指定外部配置文件

16.1.3 外部配置源

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

@Controller
public class HelloController {
    @Value("${MAVEN_HOME}")
    private String msg;
  
    @Value("os.name")
    private String osName;
}

16.2 默认配置文件

16.2.1 Spring Boot 配置加载顺序

以下是常用的 Spring Boot 配置形式及其加载顺序(优先级由高到低):

  1. 命令行参数
    Spring Boot 中的所有配置,都可以通过命令行参数进行指定,其配置形式如下:
    java -jar {Jar文件名} --{参数1}={参数值1} --{参数2}={参数值2}
    java -jar springbootdemo-0.0.1-SNAPSHOT.jar --server.port=8081 --server.servlet.context-path=/bcb
    • --server.port:指定服务器端口号
    • --server.servlet.context-path:指定上下文路径(项目的访问路径)
  2. 来自 java:comp/env 的 JNDI 属性
  3. Java 系统属性(System.getProperties())
  4. 操作系统环境变量
  5. RandomValuePropertySource 配置的 random. 属性值*
  6. 配置文件(YAML 文件、Properties 文件)
  7. @Configuration 注解类上的 @PropertySource 指定的配置文件
  8. 通过 SpringApplication.setDefaultProperties 指定的默认属性

以上所有形式的配置都会被加载,当存在相同配置内容时,高优先级的配置会覆盖低优先级的配置;存在不同的配置内容时,高优先级和低优先级的配置内容取并集,共同生效,形成互补配置

16.2.2 配置文件查找位置

  1. classpath 根路径
  2. classpath 根路径下config目录
  3. jar包当前目录
  4. jar包当前目录的config目录
  5. /config子目录的直接子目录

16.3 配置文件加载顺序

根目录 config 目录下的子文件夹 > 根目录 config 目录下 > 根目录下 > classpath 目录下的 config 目录下 > classpath 目录下
在这里插入图片描述

  1. 当前 jar 包内部的 application.properties 和 application.yml
  2. 当前 jar 包内部的application-{profile}.properties 和 application-{profile}.yml
  3. 引用的外部 jar 包的 application.properties 和 application.yml
  4. 引用的外部 jar 包的 application-{profile}.properties 和 application-{profile}.yml

指定环境优先,外部优先,后面的可以覆盖前面的同名配置项,同一位置下,Properties 文件优先级高于 YAML 文件
在这里插入图片描述

  • /myBoot:表示 JAR 包所在目录,目录名称自定义
  • /childDir:表示 JAR 包所在目录下 config 目录的子目录,目录名自定义
  • JAR:表示 Spring Boot 项目打包生成的 JAR
  • 其余带有 “/” 标识的目录的目录名称均不能修改
  • 红色数字:表示该配置文件的优先级,数字越小优先级越高

这些配置文件的优先级顺序,遵循以下规则:

  1. 先加载 JAR 包外的配置文件,再加载 JAR 包内的配置文件
  2. 先加载 config 目录内的配置文件,再加载 config 目录外的配置文件
  3. 先加载 config 子目录下的配置文件,再加载 config 目录下的配置文件
    4。 先加载 appliction-{profile}.properties/yml,再加载 application.properties/yml
  4. 先加载 .properties 文件,再加载 .yml 文件

17. 自定义 Starter

功能:传入姓名 ,自动配置前缀和后缀

17.1 创建一个 Starter 项目

创建一个空项目,加入一个 Maven 模块,作为 Starter ,再加入一个 Springboot 模块,作为 Starter-autoconfigure
在这里插入图片描述

17.2 Starter-Autoconfigure

  1. POM 配置
    将其他所有依赖和插件删掉,只留下 web-starter
    <properties>
    	<java.version>1.8</java.version>
    </properties>
    <dependencies>
    	<dependency>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter</artifactId>
    	</dependency>
    </dependencies>
    
  2. Bean
    @ConfigurationProperties("fan")
    public class HelloProperties {
        private String prefix;
        private String suffix;
    }
    
  3. Service
    无需添加进容器,即不需要 @Service 注解,注入 Bean
    public class HelloService {
        @Autowired
        HelloProperties helloProperties;
        public String sayHello(String name){
            return helloProperties.getPrefix() + ": " + name + "》" + helloProperties.getSuffix();
        }
    }
    
  4. 自动配置类 AutoConfiguration
    @Configuration
    @EnableConfigurationProperties(HelloProperties.class)
    public class HelloAutoConfiguration {
        @ConditionalOnMissingBean(HelloService.class)
        @Bean
        public HelloService helloService(){
            HelloService helloService = new HelloService();
            return helloService;
        }
    }
    
  5. 创建 META-INF 下的 spring.factories
    在这里插入图片描述
    加入自动配置类
    # Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    fan.auto.HelloAutoConfiguraion			// 自动配置类
    

17.3 Starter

  1. 引入编写好的 AutoConfiguration
    <dependency>
    	<groupId>fan</groupId>
    	<artifactId>fan-spring-boot-starter-autoconfigure</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    </dependency>
    
  2. 设置编码格式
    <properties>
    	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    

17.4 加载进本地仓库

先将 Starter-autoconfigure 执行 clean ,然后 install ,然后再将 Starter 执行 clean ,install,加载进本地仓库

17.5 使用自定义 Starter

17.5.1 引入自定义 Starter

<dependency>
		<groupId>fan</groupId>
		<artifactId>fan-spring-boot-starter</artifactId>
		<version>1.0-SNAPSHOT</version>
</dependency>

17.5.2 Controller

将自定义 Starter 中的 Service 注入进来,调用 Starter 的方法

@Controller
public class TestStarterController {
    @Autowired
    HelloService helloService;
    @ResponseBody
    @GetMapping("/teststart")
    public String sayHello(){
        String sayHello = helloService.sayHello("张三");
        return sayHello;
    }
}

17.5.3 配置项

使用 Starter-Autoconfiguration 自动配置的属性
在这里插入图片描述

fan:
  prefix: fan
  suffix: auto

17.5.4 成功返回

在这里插入图片描述

17.5.5 自定义 Config

将会使用我们自定义的 Config 来进行装配

@Configuration
public class MyHelloConfig {
    @Bean
    public HelloService helloService(){
        HelloService helloService = new HelloService();
        return helloService;
    }
}

18. Profile 功能

18.1 application-profile 功能

在 src/main/resources 下添加 4 个配置文件:

  • application.yml:主配置文件
  • application-dev.yml:开发环境配置文件
  • application-test.yml:测试环境配置文件
  • application-prod.yml:生产环境配置文件

默认配置文件 application.yml;任何时候都会加载,指定环境配置文件 application-prod.yml / application-test.yml / application-dev.yml
激活指定环境:

  • 配置文件激活
    spring:
      profiles:
        active: prod
    #    active: test
    
  • 命令行激活:(打包成JAR文件后,通过命令行激活)
    java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
    修改配置文件的任意值,命令行优先
  • 虚拟机参数激活(打包成JAR文件后,通过虚拟机参数来激活)
    java -Dspring.profiles.active=prod -jar xxx.jar

默认配置与环境配置同时生效,同名配置项,profile 配置优先

18.2 @Profile 条件装配功能

指定仅在某个 profile 下执行

@Configuration(proxyBeanMethods = false)
@Profile("prod")
public class ProductionConfiguration {
    // ...
}
@Configuration(proxyBeanMethods = false)
@Profile("test")
public class ProductionConfiguration {
    // ...
}

18.3 Profile 分组

指定 profile 分组,同时加载多个 profile ,可以分别指定值,如:一个指定 name,一个指定 age ,然后同时加载进来

spring:
	profiles:
  	active: production
		group:
    	production[0]: prodname
    	production[1]: prodage

18.4 多 Profile 文档块模式

#默认配置
server:
  port: 8080
#切换配置
spring:
  profiles:
    active: test
---
#开发环境
server:
  port: 8081
spring:
  config:
    activate:
      on-profile: dev
---
#测试环境
server:
  port: 8082
spring:
  config:
    activate:
      on-profile: test
---
#生产环境
server:
  port: 8083
spring:
  config:
    activate:
      on-profile: prod
posted @ 2022-12-21 13:52  凡223  阅读(39)  评论(0编辑  收藏  举报