注:本文基于尚硅谷雷丰阳老师公开的 SpringBoot 笔记整理而得,并加入了一些个人理解
雷丰阳老师公开笔记的地址为 :
(https://www.yuque.com/atguigu/springboot/qb7hy2)
本文中涉及到大量的源码分析部分,推荐具有设计模式的相关知识再来进行学习,
或者只学习 SpringBoot 的实际应用部分亦可
Boot,意为引导,在这里指的是引导 Spring 系列的加载配置
SpringBoot 是整合 Spring 技术栈的一站式框架
SpringBoot 是简化 Spring 技术栈的快速开发脚手架
SpringBoot 能够快速便捷的整合 Spring 家族的一系列框架,免去繁杂的配置,SpringBoot 的底层还是 Spring 框架
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
人称版本帝,迭代快,需要时刻关注变化
封装太深,内部原理复杂,不容易精通
https://github.com/spring-projects/spring-boot/wiki#release-notes
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 交互
服务围绕业务功能拆分
可以由全自动部署机制独立部署
去中心化,服务自治。服务可以使用不同的语言、不同的存储技术
JDK1.8
Maven 3.6.3
创建 Maven 工程,导入如下依赖
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.springbootdemogroupId>
<artifactId>SpringBootDemoartifactId>
<version>1.0version>
<profiles>
<profile>
<id>jdk-1.8id>
<activation>
<activeByDefault>trueactiveByDefault>
<jdk>1.8jdk>
activation>
<properties>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
<maven.compiler.compilerVersion>1.8maven.compiler.compilerVersion>
properties>
profile>
profiles>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.4.RELEASEversion>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
project>
编写 main
package com.springbootdemo.hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/*
@SpringBootApplication 使用该注解标注类,代表这是一个 SpringBoot 应用,称为主程序类
*/
@SpringBootApplication
public class HelloSpringBoot {
public static void main(String[] args) {
// 加载 SpringBoot 主类,传入 args 参数,此为固定写法
SpringApplication.run(HelloSpringBoot.class,args);
}
}
编写 controller
package com.springbootdemo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/*
@ResponseBody 的作用其实是将 java 对象转为 json 格式的数据。
此注解之后不会再走视图处理器,而是直接将数据写入到输入流中,相当于直接通过 response 响应数据
他的效果等同于通过 response 对象输出指定格式的数据。
@RestController 将 @Controller 与 @ResponseBody 结合
*/
@RestController
public class HelloController {
/*
使用 @RequestMapping 注解来响应 /hello 请求,返回字符串
*/
@RequestMapping("/hello")
public String hello() {
return "我 TM 裂开!";
}
}
注: 此处还没有配置包扫描,main 和 controller 必须要在一个包下
测试,直接运行 main,浏览器访问 http://localhost/hello
在 SpringBoot 中,所有的配置都可以写在一个配置文件 application.properties 中,例如配置 tomcat 服务端口便可以通过server.port=8888
来配置
可以添加使用 SpringBoot 的部署插件来简化部署,通过如下插件打包的 jar 包,包含 SpringBoot 运行的基本环境,包括配置文件,tomcat 等
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
通过 Maven 打包 SpringBoot 应用,在 cmd 通过如下命令便可以部署运行 SpringBoot 应用
java -jar jar包路径
依赖管理
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.4.RELEASEversion>
parent>
他的父项目
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.3.4.RELEASEversion>
parent>
几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制
若要使用某种场景,只需要导入即可,例如需要使用 web 场景
1、springboot 的 pom 中有很多 spring-boot-starter-* : *就代表某种场景
2、只要引入 starter,这个场景的所有常规需要的依赖我们都自动引入
3、SpringBoot 所有支持的场景
https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter
4、见到的 *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。
5、所有场景启动器最底层的依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
<version>2.3.4.RELEASEversion>
<scope>compilescope>
dependency>
无需关注版本号,自动版本仲裁
1、引入依赖默认都可以不写版本
2、引入非版本仲裁的 jar,要写版本号
也可以修改版本号,根据的是 maven 的最近优先原则,当前 pom 中有对应参数,则使用当前 pom 中的参数
1、查看spring-boot-dependencies里面规定的当前依赖版本用的 key。
2、在当前项目里面重写配置
<properties>
<mysql.version>5.1.43mysql.version>
properties>
自动配置好 tomcat,如下
引入Tomcat依赖
配置Tomcat
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
<version>2.3.4.RELEASEversion>
<scope>compilescope>
dependency>
自动配置好 SpringMVC
自动配置好 Web 常见功能,例如字符编码的问题
默认的包结构
主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
无需以前的包扫描配置
想要改变扫描路径,可使用 @SpringBootApplication(scanBasePackages=“com.atguigu”)
或者 @ComponentScan 指定扫描路径
@SpringBootApplication
等同于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot")
各种配置拥有默认值
按需加载所有自动配置项目
#############################Configuration使用示######################################################
package com.springbootdemo.demo01.config;
import com.springbootdemo.demo01.bean.Cat;
import com.springbootdemo.demo01.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/*
1.告诉 SpringBoot 这是一个配置类
2.配置类本身也是组件
proxyBeanMethods 代理配置类中被 @Bean 修饰的方法
*/
@Configuration(proxyBeanMethods = true)
public class MyConfig {
/*
使用 @Bean 向容器中添加组件,方法名作为该组件在容器中的 id,方法的返回值作为该组件在容器中的实例
*/
@Bean
public Person person01() {
return new Person("1001", "张三", 18);
}
// 也可以通过 @Bean 注解的参数来指定容器实例的 id
@Bean("person03")
public Person person02() {
return new Person("1002", "李四", 20);
}
@Bean("person04")
public Person person03() {
Person jerry = new Person("1003", "Jerry", 19);
// 此时,产生了组件依赖,person04 依赖了 cat01 组件,在 proxyBeanMethods = true 的情况下成立
jerry.setCat(cat01());
return jerry;
}
@Bean("cat01")
public Cat cat01() {
return new Cat("tom");
}
}
################################@Configuration测试代码如下########################################
package com.springbootdemo.demo01.boot;
import com.springbootdemo.demo01.bean.Person;
import com.springbootdemo.demo01.config.MyConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication(scanBasePackages = "com.springbootdemo.demo01")
public class DemoSpringBoot {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(DemoSpringBoot.class, args);
// 获取 IOC 容器中所有的组件名称,返回一个数组集合
String[] beanDefinitionNames = run.getBeanDefinitionNames();
// 查找打印刚刚添加的 Person 容器实例
for (String beanDefinitionName : beanDefinitionNames) {
if (beanDefinitionName.contains("person")) {
System.out.println(beanDefinitionName);
}
}
// 获取一个组件,因为添加的组件默认为单实例的,多次获取,都是同一组件实例
Person peron03 = run.getBean("person03", Person.class);
System.out.println(peron03);
// 获取到的配置类对象,本身就是一个被 Spring 增强过的代理对象
MyConfig conf = run.getBean(MyConfig.class);
/*
使用该代理对象来获取容器实例,无论获取多少次,依然获取的是单实例
因为配置类的 @Configuration 注解默认设置了 proxyBeanMethods 属性为 true,@Configuration(proxyBeanMethods = true)
每次代理类对象调用方法获取容器中的组件,SpringBooot 总会检查该组件是否在容器中有,保证容器的单实例
若设置 proxyBeanMethods 为 false ,SpringBoot 就不会对调用的方法进行代理,返回的是多实例
根据该属性的不同值,可以将 @Configuration 分为两种配置模式
proxyBeanMethods = true -> Full 全配置
proxyBeanMethods = false -> Lite 轻量级配置
*/
Person person = conf.person01();
Person person1 = conf.person01();
System.out.println(person == person1); // true
}
}
补充:其余的 @Component、@Controller、@Service、@Repository 等添加组件的注解依然可以使用
@Import
/*
@Import 给容器中自动创建出 @Import 参数中指定类型的多个组件
默认获取的组件的名字就是该类型的组件的全类名
通过导入自定义的 ImportSelector 实现类 MySelect.class 来实现批量导入组件
通过导入自定义的 ImportBeanDefinitionRegistrar 实现类 MyImportBean.class 来实现批量导入组件
*/
@Import({
DBHelper.class, Person.class, MySelect.class, MyImportBean.class})
ImportSelector
package com.springbootdemo.demo01.importselect;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
/*
实现 ImportSelector 接口,能够将多个需要导入到容器中的组件批量导入
selectImports() 该方法的返回值是一个数组类型,保存的就是多个组件的全类路径
AnnotationMetadata 当前标注 @Import 注解的所有注解信息
在 @Import 中进行注册使用
*/
public class MySelect implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 不要返回 null 值,若没有数据,可以返回空数组
return new String[]{
"com.springbootdemo.demo01.bean.Cat","com.springbootdemo.demo01.bean.Person"};
}
}
ImportBeanDefinitionRegistrar
package com.springbootdemo.demo01.importbean;
import com.springbootdemo.demo01.bean.Person;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
public class MyImportBean implements ImportBeanDefinitionRegistrar {
/*
AnnotationMetadata 当前类的注解信息
BeanDefinitionRegistry Bean 定义注册类
把所有添加到容器中的 Bean , 通过调用 BeanDefinitionRegistry 的 registerBeanDefinition 方法进行组件的手动注册
在 @Import 中进行注册使用
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 通过组件 id 判断容器中是否有对应的组件,若有,创建一个 person02 组件,添加到容器
if (registry.containsBeanDefinition("person01")) {
// 需要通过 BeanDefinition 来传入需要添加的组件类型 Person.class,指定 Bean 的定义信息
BeanDefinition beanDefinition = new RootBeanDefinition(Person.class);
registry.registerBeanDefinition("person02", beanDefinition);
}
}
}
条件装配,满足 Conditional 的指定条件时,则进行组件注入
代码演示
/*
@Conditional 条件装配,满足 Conditional 的指定条件时,则进行组件注入
@ConditionalOnBean 当容器中有 id 为 cat01 的组件时,才进行如下组件的注册,该注解也可以修饰类
只有当满足条件时,类中的组件注册才会执行,否则都不执行
使用 @ConditionalOnBean 能够保障组件的依赖
*/
@ConditionalOnBean(name = "cat01")
@Bean
public Person person04() {
Person person = new Person("1004", "王五", 22);
person.setCat(cat01());
return person;
}
/*
@ImportResource 通过参数指定的路径,可以导入外部的 Spring 配置文件
*/
@ImportResource("classpath:bean.xml")
如何使用 Java 读取到 properties 文件中的内容,并且把它封装到 JavaBean 中,以供随时使用
// 配置文件中有对应属性字段
mycar.brand=BYD
mycar.price=100000
代码演示
/**
* 只有在容器中的组件,才会拥有 SpringBoot 提供的强大功能
*/
@Component // 组件名称默认为类名小写
@ConfigurationProperties(prefix = "mycar") // 在需要绑定配置的类上面添加注解,提供前缀匹配对应配置字段
public class Car {
/*
根据指定前缀开头,对配置配置文件的属性名与 JavaBean 的属性名进行匹配和属性值注入
*/
private String brand;
private Integer price;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
", price=" + price +
'}';
}
}
演示
package com.dhj.profile;
import com.dhj.profile.bean.Person;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ProfileApplicationTests {
/*
注:使用时通过配置文件注入属性的实例时,,需要通过 @Autowired 来将对应的实例注入,否则配置文件的绑定会失效
这也是 Spring IOC 的深刻体现,通过 Spring 容器来管理实例对象,容器中的对象能够使用 Spring 提供的各种功能,
例如配置文件绑定,属性注入等
*/
@Autowired
Car car;
@Test
void contextLoads() {
System.out.println(car.getBrand().toString());
}
}
代码演示
/*
@EnableConfigurationProperties
1. 通过参数,为指定类型的组件,开启属性配置
2. 同时将该组件自动注入容器中
*/
@EnableConfigurationProperties(Car.class)
代表当前是一个配置类
指定扫描那些类
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
自动配置包,指定默认的包规则
@Import(AutoConfigurationPackages.Registrar.class) //给容器中导入一个组件
public @interface AutoConfigurationPackage {
}
//利用 Registrar 给容器中导入一系列组件
//将指定的一个包下的所有组件导入进来? 还是只导入 MainApplication 所在包下。
1、利用 getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
2、调用 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类
3、利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) 得到所有的组件
4、从 META-INF/spring.factories 位置来加载一个文件。
默认扫描我们当前系统里面所有META-INF/spring.factories 位置的文件
spring-boot-autoconfigure-2.3.4.RELEASE.jar 包里面也有META-INF/spring.factories
文件里面写死了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
虽然我们 127 个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration 是按照条件装配规则(@Conditional)来装配的,最终会按需配置。
@Bean
@ConditionalOnBean(MultipartResolver.class) //容器中有这个类型组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名字 multipartResolver 的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// 给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
// SpringMVC multipartResolver。防止有些用户配置的文件上传解析器名称不符合规范,则 SpringBoot 自己去容器中查找 MultipartResolver,并根据方法名,返回一个命名规范的 MultipartResolver
// Detect if the user has created a MultipartResolver but named it incorrectly
// (/检测用户是否创建了一个 MultipartResolver ,但命名不正确)
return resolver;
}
给容器中加入了文件上传解析器;
SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先
例如,用户自己编写了字符编码的配置加载到容器中,则 SpringBoot 以用户配置的优先
@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
}
SpringBoot 先加载所有的自动配置类 xxxxxAutoConfiguration
每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties 里面拿。xxxProperties 和配置文件进行了绑定
生效的配置类就会给容器中装配很多组件
只要容器中有这些组件,相当于这些功能就有了
定制化配置
用户直接自己在配置类中通过 @Bean 替换底层的组件
用户去看这个组件是获取的配置文件什么值就去修改,例如在 application.properties 中去修改字符编码集的值
server.servlet.encoding.charset=UTF-8
xxxxxAutoConfiguration 类 --> 转化为组件 --> xxxxProperties 类里面拿值 —> application.properties 或 application.yaml 配置文件
我们可以将自动配置的关键几步以及相应的注解总结如下:
1、@Configuration 与 @Bean:基于 Java 代码的 bean 配置,@Configuration 相当于原生 Spring 中的 xml 配置文件,@Bean 就相当于
标签
2、@Conditional:设置自动配置条件依赖
3、@EnableConfigurationProperties 与 @ConfigurationProperties:读取配置文件转换为 bean
4、@EnableAutoConfiguration 与 @Import:实现 bean 发现与加载
当我们引入了一个 starter 后,springboot 项目启动,启动类上标注了 @SpringBootApplication 注解,该注解包含三个重要注解:
1、@SpringBootConfiguration(标注springboot 启动类为一个配置类),相当于 @Configuration 注解
2、@ComponentScan 默认会扫描 springboot 项目启动类所在包及其子包的所有符合条件的组件
3、@EnableAutoConfiguration 该注解就主要用于自动配置和引入相关依赖中的组件
在 @EnableAutoConfiguration 注解中,又引入了 @Import,其作用为:导入需要自动配置的组件
@Import 注解的默认参数为 AutoConfigurationImportSelector.class(自动配置导入选择器)
在 AutoConfigurationImportSelector 中的 getCandidateConfigurations() 方法里,调用了 SpringFactoriesLoader 类中的 loadFactoryNames() 方法
该方法可以从所有引入的 jar 包中,包括所有引入的 stater 依赖,或者是自己引入的其他 maven 依赖的 jar 包中,读取 META-INF/spring.factories 文件,在该文件中,就定义了该 jar 包的自动配置类的类全路径,自动配置类通过配置文件属性类加载自动配置所需的配置文件参数,规定参数的前缀;设定自动配置的条件,如使用 @ConditionalOnBean | ConditionalOnMissingClass 等注解,最终完成自动配置
springboot 通过 META-INF/spring.factories 文件找到并加载这些自动配置类,就可以完成对其引入的 starter 和其他 maven 依赖的自动配置
注意:正如上面所说,要完成自动配置,引入的 jar 包中,就需要有 META-INF/spring.factories 文件且正确,自动配置类的配置条件也要满足,比如 mybatis 的自动配置,就需要满足数据库连接池的配置条件,配置文件中也需要进行相关的配置
引入场景依赖
查看自动配置了哪些(选做)
自己分析,引入场景对应的自动配置一般都生效了
配置文件中 debug=true 开启自动配置报告。Negative(不生效)\ Positive(生效)
是否需要修改
@Bean、@Component …
自定义器 XXXXXCustomizer;…
简化 JavaBean 的开发
引入依赖
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
IDEA 插件市场搜索 lombok 插件进行安装
lombok 使用
package com.springbootdemo.demo02.lombokdemo;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
@Data // 使用 @Data 注解,在编译时自动生成 get | set 方法
@AllArgsConstructor // 使用 @AllArgsConstructor 注解,在编译时自动生成有参构造方法(默认是全参),若需要定制有参,取消该注解,手写有参即可
@NoArgsConstructor // 生成无参构造方法
@ToString // 生成 ToString 方法
@EqualsAndHashCode // 生成 equals 和 hashcode 方法
@Slf4j // 引入一个日志类,可以使用 log 来打印日志
public class Stu {
private String id;
private String name;
}
实现项目的热更新
引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<optional>trueoptional>
dependency>
使用方法
修改了项目后,使用 Ctrl + F9 实时热更新项目,若没有修改项目,则不会实现热更新
补充,严格来说,这并不是热更新,只是在检测到项目变化时,重启项目,要使用热更新功能,推荐 JRebel ( 收费 )
同上面所涉到的 properties 用法相同
YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。
非常适合用来做以数据为中心的配置文件
字面量,单个的、不可再分的值。date、boolean、string、number、null
k: v
对象,键值对的集合。map、hash、set、object
行内写法: k: {k1:v1,k2:v2,k3:v3}
#或
k:
k1: v1
k2: v2
k3: v3
数组,一组按次序排列的值。array、list、queue
行内写法: k: [v1,v2,v3]
#或者
k:
- v1
- v2
- v3
使用 yaml 表示如下对象
@Data
public class Person {
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}
@Data
public class Pet {
private String name;
private Double weight;
}
对应的 yaml 文件
# yaml表示以上对象
person:
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
interests: [篮球,游泳]
animal:
- jerry
- mario
score:
english:
first: 30
second: 40
third: 50
math: [131,140,148]
chinese: {
first: 128,second: 136}
salarys: [3999,4999.98,5999.99]
allPets:
sick:
- {
name: tom}
- {
name: jerry,weight: 47}
health: [{
name: mario,weight: 47}]
自定义的类和配置文件之间,一般没有提示
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
使用以上依赖,能够在编写自定义类的配置文件时,出现提示,提示使用以上插件,使得在打包项目是,不会将配置处理器打包到 jar 包中
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.
Support for serving static resources, including support for WebJars (covered later in this document)).
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.
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 typeWebMvcConfigurer
but without@EnableWebMvc
.不用@EnableWebMvc注解。使用
@Configuration
+WebMvcConfigurer
自定义规则
If you want to provide custom instances of
RequestMappingHandlerMapping
,RequestMappingHandlerAdapter
, orExceptionHandlerExceptionResolver
, and still keep the Spring Boot MVC customizations, you can declare a bean of typeWebMvcRegistrations
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
-annotatedDelegatingWebMvcConfiguration
as described in the Javadoc of@EnableWebMvc
.使用
@EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC
只要静态资源放在类路径下(也就是项目结构的 resource 目录下),且名为 /static
(or /public
or /resources
or /META-INF/resources
等
访问时,通过 当前项目根路径 / + 静态资源名 就可访问
原理:静态资源映射的是 /**
请求进来,先去找 Controller 看是否能处理,不能处理的所有请求,又都交给静态资源处理器,若静态资源也找不到,返回 404
在 yaml 配置文件中进行配置即可
spring:
mvc:
static-path-pattern: /res/**
配置后,静态资源访问效果为: /res/**,相当于: / + static-path-pattern的值 + / 静态资源名
spring:
web:
resources:
static-locations: classpath:/haha #配置静态资源存放路径
通过配置指定的静态资源存放路径后,需要将静态资源转移或存放到配置的路径下,若配置了访问前缀,则加上访问前缀来访问,若没有配置访问前缀,则直接通过 / + 资源名来访问
小问题: 尽管配置写对了,可能会出现 404 无法访问的问题,是因为 maven 之前的缓存还在,clearn 一下 maven 即可
jquery 等 webjar 自动映射到: /webjars/**
页面访问
依赖
<dependency>
<groupId>org.webjarsgroupId>
<artifactId>jqueryartifactId>
<version>3.5.1version>
dependency>
只要在任意配置了的静态资源路径下的放置名为 index.html 的静态资源文件,在访问项目路径 http://localhost:8080/ 时,该静态文件便可作为欢迎页被默认首先访问
注意
可以配置静态资源路径
但是不可以配置静态资源的访问前缀,否则导致 index.html 不能正常访问
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效
controller 能处理 /index.html
自定义访问网页时,网页标签上显示的图标
将一个图片命名为 favicon.ico ,放置到任意配置过的静态资源文件夹下,通过浏览器访问,便能看到浏览器标签页的图标
若没有看到图片,清除浏览器缓存或者换个浏览器访问即可,同时,自定义配置静态资源的访问前缀也会影响 favicon 的访问
SpringBoot 启动,默认会加载 xxxAutoConfiguration 类
SpringMVC 功能的自动配置类就叫 WebMvcAutoConfiguration ,会被 SpringBoot 加载
@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 {
}
加载 WebMvcAutoConfiguration 后,向容器中配置了如下
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({
WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
}
使用 @ConfigurationProperties 注解,将配置文件的相关属性和容器中相关的类进行了绑定,例如容器中 WebMvcProperties 类通过 @ConfigurationProperties(prefix = “spring.mvc”) 注解,与配置文件中 spring.mvc 前缀开头的属性进行了绑定;ResourceProperties 类 spring.resource 属性进行了绑定等
配置类只有一个有参构造器
/*有参构造器所有参数的值都会从容器中确定
例如:
ResourceProperties resourceProperties;获取和 spring.resources 绑定的所有的值的对象
WebMvcProperties mvcProperties 获取和 spring.mvc 绑定的所有的值的对象
ListableBeanFactory beanFactory Spring 的 beanFactory
HttpMessageConverters 找到所有的 HttpMessageConverters
ResourceHandlerRegistrationCustomizer 找到资源处理器的自定义器
DispatcherServletPath
ServletRegistrationBean 给应用注册Servlet、Filter 等等*/
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 当配置文件中 spring.resource.add-mappings 的值为 false 时,所有的静态资源都会被禁用
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:
resources:
add-mappings: false 禁用所有静态资源规则
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
// SpringBoot 定义的默认的四种静态资源访问路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。
@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");
}
// 若欢迎页不存在或者不是 /** 的请求规则,则将请求 /index 交给 Controller 来处理
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
favicon 的请求是由浏览器默认发送的,请求为 /favicon.ico
在当前版本的 SpringBoot(2.3.4),针对欢迎页和 favicon 二者的静态资源处理,根据源码来看,是不能配置访问前缀的
通过 @xxxMapping 等注解来映射处理请求
Rest 风格的请求
使用 HTTP 请求方式的动词来表示对资源的不同操作
对比 Rest 和 普通请求
普通请求
- /getUser 获取用户
- /deleteUser 删除用户
- /editUser 修改用户
- /saveUser 保存用户
Rest 风格的请求
- /user ,POST 请求-保存用户;DELETE 请求-删除用户;GET 请求-获取查询用户;PUT 请求-修改用户
核心在于 HiddenHttpMethodFilter,通过 HiddenHttpMethodFilter 将普通的 POST 请求拦截转化为 Rest 风格的请求
用法
表单 method=post,隐藏域 _method=put
需要在 SpringBoot 中手动开启
spring: mvc: hiddenmethod: filter: enabled: true #开启 Rest 风格
在 @xxxMapping 注解中选择接收那种 Rest 风格的请求,例如
@RequestMapping(value = "/user",method = RequestMethod.PUT) // 接收 GET 类型的 Rest 请求
在页面上设置发送的请求
<form action="/user" method="post"> <input name="_method" value="PUT" type="hidden"> <input type="submit" value="提交"> form>
扩展
如何将默认的 _method 参数更换成我们喜欢的
自定义一个 HiddenHttpMethodFilter 放入容器中,替换默认的 HiddenHttpMethodFilter,实现 _method 的更换
//在配置类中,自定义 filter 放入容器中 @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); // 设置规定请求方式的参数为 _m methodFilter.setMethodParam("_m"); return methodFilter; }
补充
还可以通过新的 @xxxMapping 等一系列注解替换 @RequestMapping 注解对应的 Rest 风格请求接收的的繁杂书写
// Get 请求 @GetMapping(value = "/user") public String getUser() { return "GET-张三"; } // Post 请求 @PostMapping(value = "/user") public String saveUser() { return "POST-张三"; } // Put 请求 @PutMapping(value = "/user") public String putUser() { return "PUT-张三"; } // Delete 请求 @DeleteMapping(value = "/user") public String deleteUser() { return "DELETE-张三"; }
每次发生请求,是如何找到那个方法来处理对应的请求呢?
快捷键补充
如上图,原生的 Servlet 请求被 SpringBoot 经过各种继承体系,最终通过org.springframework.web.servlet.DispatcherServlet 中 doDispatch() 方法来处理请求,部分源码如下
// 使用该方法来进行请求的处理分发到具体的 Controller 的对应目标方法
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(controller) 的映射规则,如下图
总结:
SpringBoot 自动配置了欢迎页的 WelcomePageHandlerMapping,通过访问 / 能访问到 index.html;
SpringBoot 自动配置了默认 的 RequestMappingHandlerMapping
请求进来,挨个尝试所有的 HandlerMapping 看是否有请求信息
如果我们需要一些自定义的映射处理,我们也可以自己向容器中放如 HandlerMapping,实现自定义 HandlerMapping
@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
使用演示
package com.dhj.web01.controller; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.*; import javax.servlet.http.Cookie; import java.awt.image.ImageProducer; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @RestController public class ParamController { @GetMapping("/car/{id}/owner/{username}") /* 通过 @PathVariable 注解,将请求的 url 中的指定部分通过 {} 映射为参数,该注解只能使用在方法的参数 列表中 也可以在方法的参数列表中通过定义一个 Map
map 来接收 url 中,K V 类型必须为 String 类型 所有经过映射的参数,通过 key 来获取 */ public Map<String, Object> getCar(@PathVariable("id") Integer id, @PathVariable("username") String name, @PathVariable Map<String, String> varMap, /* 通过 @RequestHeader 注解,传入请求头对应的 key 来获取请求头 的信息 */ @RequestHeader("User-Agent") String user_agent, /* @RequestHeader 还可以通过在方法参数列表中定义 Map类型的参数 获取所有的请求头信息 */ @RequestHeader Map<String, String> headerMap, /* 通过 @RequestParam 注解,传入请求 url 中的参数名来获取请求 url 中指定参数的值 若同一个参数由多个值,则可以通过 List 类型来获取 */ @RequestParam("age") Integer age, @RequestParam("inters") List<String> inters, /* 同样的, @RequestParam 也支持将所有的请求参数封装到一个 Map中去 由于这里的普通 Map 针对多个相同参数名作为 key 情况下,只能拿 到一个 value 值,这里可以使用 MultiValueMap @RequestParam MultiValueMap<String, String> paramMap, /* @CookieValue 通过在注解参数中传入 cookie 的 key 来获取请求 中,只 cookie 的值 */ @CookieValue("_ga") String cookVal, /* 也可以将参数的类型申明为原生的 Cookie,拿到指定 Cookie 的所 有信息 */ @CookieValue("_ga") Cookie cookie) { // @PathVariable测试,获取映射的请求路径中,参数的值 Map<String, Object> map = new HashMap<>(); map.put("id", id); map.put("username", name); // 将参数中封装好的 mapVar 也存入返回值的 map 中,方便查看页面的效果 map.put("kv", varMap); System.out.println("======= 请求头 Map ======="); // @RequestHeader测试, 遍历所有的请求头信息 Iterator<Map.Entry<String, String>> iterator = headerMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, String> next = iterator.next(); System.out.println(next.getKey() + ":" + next.getValue()); } // @RequestParam测试, 遍历所有请求参数的 MultiValueMap 集合 System.out.println("========= 参数 Map ========="); Iterator<Map.Entry<String, List<String>>> iterator1 = paramMap.entrySet().iterator(); while (iterator1.hasNext()) { Map.Entry<String, List<String>> next = iterator1.next(); List<String> values = next.getValue(); for (String value : values) { System.out.println(next.getKey() + ":" + value); } } return map; } // 补充,可以在参数注解中添加 required = false 来使得参数可以在请求域中不存在,例如 @RequestAttribute(value = "msg", required = false); @PathVariable(value = "id",required = false); 等等 //=======@RequestBody 演示,该注解针对的是 Post 请求,使用该注解,直接获取的是请求体的内容======== @PostMapping("/save") public Map<String, Object> reqBody(@RequestBody String content) { HashMap<String, Object> map = new HashMap<>(); map.put("content", content); System.out.println(content); return map; } } //===============================分割线======================================================= package com.dhj.web01.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @Controller public class RequestController { @GetMapping("/goto") public String goToPage(HttpServletRequest req) { req.setAttribute("msg", "成功请求"); req.setAttribute("code", 200); return "forward:success"; // 请求转发到 /success } @ResponseBody @GetMapping("/success") public Map<String, Object> success( /* 通过 @RequestAttribute 注解,向注解参数传入请求的属性名,来获取请求的属性值 */ @RequestAttribute("msg") String msg, @RequestAttribute("code") Integer code, HttpServletRequest req) { // 也可以通过原生 request 对象来获取请求的属性值 Object msg1 = req.getAttribute("msg"); Object code1 = req.getAttribute("code"); Map<String, Object> map = new HashMap<>(); map.put("msg", msg); map.put("code", code); return map; } } //=================================分割线===================================================== /* 矩阵变量 矩阵变量可以出现在任何路径片段中,每一个矩阵变量都用分号 ; 隔开。比如 /color=red;year=2012 多个值可以用逗号隔开,比如 color=red,green,blue 或者分开写 color=red;color=green;color=blue 分号前面为正常的请求路径,如 /user/stu;age=10;scope=100 */ /* SpringMVC 中可以使用 @MatrixVariable 注解处理带矩阵变量的请求 url 注意: SpringBoot 默认禁用了矩阵变量处理 */ //===================首先,通过自定义的配置类,启用矩阵变量的处理================================ package com.dhj.web01.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.util.UrlPathHelper; @Configuration(proxyBeanMethods = false) public class WebConfig { // 设置自定义的 WebMvcConfigurer 类 @Bean public WebMvcConfigurer webMvcConfigurer() { // 返回自定义的 WebMvcConfigurer ,重写了其中的 configurePathMatch,使其能够处理带矩阵变量的请求 return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 设置不设置删除分号后内容,实现矩阵路径的完整请求 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } }; } } //============================编写矩阵变量的测试 controller==================================== package com.dhj.web01.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import org.w3c.dom.stylesheets.LinkStyle; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.List; import java.util.Map; @Controller public class RequestController { /* 矩阵变量的请求路径 /cars/shell;low=23;brand=byd,audi,yd 使用 @MatrixVariable 注解来获取矩阵变量中的参数值 注意:矩阵变量是绑定到路径变量中的,因此,目标方法接收请求的格式,因该将矩阵变量的部分写为路径变量的形式 如下 */ @ResponseBody @GetMapping("/cars/{path}") public Map<String, Object> cars(@MatrixVariable("low") Integer low, @MatrixVariable("brand") List<String> brands, @PathVariable("path") String path) { Map<String, Object> map = new HashMap<>(); map.put("low", low); map.put("brands", brands); System.out.println(path); return map; } /* 矩阵变量测试2 @MatrixVariable(pathVar = "path1", value = "age") 使用如上写法,可以获取指定的路径变量中指定的参数的值 避免因为多个路径变量中存在多个相同参数名的参数而在获取时,产生歧义 */ @ResponseBody @GetMapping("/boss/{path1}/{path2}") public Map<String, Object> boss(@MatrixVariable(pathVar = "path1", value = "age") String age1, @MatrixVariable(pathVar = "path2", value = "age") String age2) { Map<String, Object> map = new HashMap<>(); map.put("age1", age1); map.put("age2", age2); return map; } }来获取多个相同参数名的值 */
大致步骤:
HandlerMapping 中找到能处理请求的 Handler(Controller.method())
为当前 Handler 找一个适配器 HandlerAdapter; RequestMappingHandlerAdapter
适配器执行目标方法并确定方法参数的每一个值
// 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);
确定将要执行的目标方法的每一个参数的值是什么,SpringMVC目标方法能写多少种参数类型、取决于参数解析器
对各种返回值进行处理
============在 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;
}
// 挨个判断所有参数解析器那个支持解析这个参数
@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;
}
解析参数: 调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可
SpringMVC 也支持解析如下类型的 ServletAPI 参数
@Override public boolean supportsParameter(MethodParameter parameter) { Class<?> paramType = parameter.getParameterType(); return (WebRequest.class.isAssignableFrom(paramType) || ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType) || HttpSession.class.isAssignableFrom(paramType) || (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) || Principal.class.isAssignableFrom(paramType) || InputStream.class.isAssignableFrom(paramType) || Reader.class.isAssignableFrom(paramType) || HttpMethod.class == paramType || Locale.class == paramType || TimeZone.class == paramType || ZoneId.class == paramType); }
Map、Model(map、model里面的数据会被放在 request 的请求域 request.setAttribute)、Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
Map<String,Object> map, Model model, HttpServletRequest request 都是可以给 request 域中放数据的 request.getAttribute();
Map、Model类型的参数,会返回 mavContainer.getModel();—> BindingAwareModelMap 是Model 也是Map,mavContainer.getModel(); 获取到值的
使用 ServletModelAttributeMethodProcessor 这个参数处理器来解析封装自定义类型,Pojo
判断是否为简单类型
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带来参数的字符串)转换到指定的类型,例如 String --> Integer,byte --> file
通过源码分析,可以看出,未来我们可以给 WebDataBinder 里面放自己的 Converter
private static final class StringToNumber<T extends Number> implements Converter<String, T>
例如
//1、WebMvcConfigurer定制化 SpringMVC 的功能
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) {
/*
阿猫,3
将如上请求的参数以指定的 , 分隔符分开,赋值给 pet 对象中的对应属性
*/
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;
}
});
}
};
}
相关的 json 场景,SpringBoot 已经在 spring-boot-starter-web 中自动引入
使用 @ResponceBody 注解可以将 Java 对象转化为 json 数据响应到页面
使用返回值解析器解析数据
try {
// 解析返回值
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
// handleReturnValue 具体实现
@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);
}
返回值解析器原理
返回值处理器判断是否支持这种类型返回值 supportsReturnType
返回值处理器调用 handleReturnValue 进行处理
RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。
利用 MessageConverters 进行处理 将数据写为 json
内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
得到 MappingJackson2HttpMessageConverter 可以将对象写为 json
利用 MappingJackson2HttpMessageConverter 将对象转为 json 再写出去
SpringMVC 支持的返回值
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
HttpMessageConverter: 看是否支持将 此 Class 类型的对象,转为 MediaType 类型的数据。
例如:Person对象转为JSON。或者 JSON 转为 Person
默认的 MessageConverter
0 - 只支持 Byte 类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
6 - MultiValueMap
7 - true 支持对任意对象进行转换
8 - true 支持对任意对象进行转换
9 - 支持注解方式xml处理的
根据客户端接受能力的不同,SpringBoot 自动返回不同类型的数据
引入依赖,测试返回 xml 数据
<dependency> <groupId>com.fasterxml.jackson.dataformatgroupId> <artifactId>jackson-dataformat-xmlartifactId> dependency>
通过浏览器和 PostMan 二者发送请求的对比( postman: json | 浏览器: xml ),得出结论: 只需要改变请求头中 Http 协议中规定的 Accept 字段告诉服务器本客户端可以接收的数据类型,SpringBoot 就会自动返回不同类型且与请求头对应的数据类型
判断当前响应头中是否已经有确定的媒体类型。MediaType
获取客户端(PostMan、浏览器)支持接收的内容类型,获取客户端 Accept 请求头字段: application/xml
遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
找到支持操作 Person 的 converter,把 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()));
}
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()));
}
若浏览器发请求,返回 xml 数据:[ application/xml ] 使用 jacksonXmlConverter 转换器
若 ajax 请求,返回 json 数据:[ application/json ] 使用 jacksonJsonConverter 转换器
若自定义的硅谷客户端发送请求,返回自定义协议的数据: [ application/x-guigu ] 使用 xxxConverter 转换器
大致流程
实现多协议数据兼容。json、xml、x-guigu
0、@ResponseBody 响应数据出去调用 RequestResponseBodyMethodProcessor 处理
1、Processor 处理方法返回值。通过 MessageConverter 处理
2、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
3、内容协商找到最终的 messageConverter;
实现步骤
添加自定义的 MessageConverter 进系统底层
系统底层会统计出所有的 MessageConverter 能操作的那些类型
客户端内容协商( 客户端将自己可以接收的类型与服务器能够响应的类型进行协商 ),例如客户端需要 json ,而服务端有 jacksonJsonConverter 能够对客户端需要的 json 数据进行处理和转化,内容协商成功,最终返回数据给客户端
代码实现
编写自定义的 convarter
package com.dhj.web01.convarter;
import com.dhj.web01.bean.Person;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
// 添加自定义的 Convarter 实现自定义数据类型的内容协商,处理的是 Person 对象数据
public class MyConvarter implements HttpMessageConverter<Person> {
// 该方法针对 @RequestBody 传入进来的数据是否能够进行内容协商
@Override
public boolean canRead(Class<?> aClass, MediaType mediaType) {
return false;
}
// 该方法针对 @ResponseBody 响应出去的数据是否能够进行内容协商
@Override
public boolean canWrite(Class<?> aClass, MediaType mediaType) {
// 只要目标方法返回的值为 Person,则可以进行内容协商
return aClass.isAssignableFrom(Person.class);
// return true; // 或者直接返回 true
}
/*
服务器需要统计所有的 MessageConverter 都能写出那些内容
通过 getSupportedMediaTypes() 获取支持的媒体类型
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
// 将字符串转化为自定义的支持媒体类型
return MediaType.parseMediaTypes("application/x-guigu");
}
@Override
public Person read(Class<? extends Person> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
// 这里不对传入的数据进行处理,直接返回 null
return null;
}
@Override
public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
/*
将自定义协议数据类型以指定格式写出
将 Person 中的数据以指定分隔符分割
*/
String data = person.getId() + "," + person.getName() + "," + person.getAge();
// 获取一个 body 输出流对象
OutputStream body = httpOutputMessage.getBody();
// 使用 body 字节流写出数据
body.write(data.getBytes());
}
}
添加自定义的 Conveter
// 在配置类中,实现自定义的 extendMessageConverters 扩展消息转换器,也就是实现内容协商的用户自定义 converters,该 converter 会被加入 SpringBoot 容器中并在内容协商时被 SpringBoot 扫描使用到
// 添加自定义的 Converter 到容器中
@Bean
public WebMvcConfigurer webMvcConfigurer1() {
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyConvarter());
}
};
}
// 补充,对于 SpringMVC 的各种功能,都可以通过在配置类中添加 WebMvcConfigurer 来实现功能的自定义和添加,WebMvcConfigurer 只能添加一个,可以在其中重写多个代表对应的方法来实现自定义 SpringMVC 功能
package com.dhj.web01.config;
import com.dhj.web01.convarter.MyConvarter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Configuration(proxyBeanMethods = false)
public class WebConfig {
// 设置自定义的 WebMvcConfigurer 类
@Bean
public WebMvcConfigurer webMvcConfigurer() {
// 返回自定义的 WebMvcConfigurer ,重写了其中的 configurePathMatch,使其能够处理带矩阵变量的请求
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 设置不设置删除分号后内容,实现矩阵路径的完整请求
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyConvarter());
}
// 指定内容协商的类型
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
/*
将需要指定的协商类型存入 Map 集合中,key 为 浏览器传入参数时,需要指定的协商类型值
value 就为具体的 MediaType 类型实例
*/
Map<String, MediaType> map = new HashMap<>();
map.put("json", MediaType.APPLICATION_JSON);
map.put("xml", MediaType.APPLICATION_XML);
map.put("gg", MediaType.parseMediaType("application/x-guigu"));
/*
SpringMVC 支持两种内容协商的方式,一种为请求头 new HeaderContentNegotiationStrategy()
一种就为请求的参数,如下,此处通过请求的参数设置支持的协商数据类型
在上面设置了 json | xml | 自定义的 gg 类型
*/
ParameterContentNegotiationStrategy p = new ParameterContentNegotiationStrategy(map);
//同时,也可以将基于请求头的内容协商管理器加上
HeaderContentNegotiationStrategy h = new HeaderContentNegotiationStrategy();
// 设置协商的策略为参数协商和请求头协商
configurer.strategies(Arrays.asList(p, h));
//http://localhost:8080/test/person?format=gg 通过如上设置,就可以通过参数 format=gg 来指定内容协商的数据类型
}
};
}
}
// 前提是需要有自定义的内容协商 Converter 类
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效
大家考虑,上述功能除了我们完全自定义外?SpringBoot 有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】
视图解析: SpringbBoot 默认不支持 jsp,需要引入第三方模板引擎技术来实现页面渲染
视图的处理方式有: 转发与重定向,以及自定义视图
thymeleaf 简介
Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.
一个现代化、服务端 Java 模板引擎
基本语法
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域、session域、对象等值 |
选择变量 | *{…} | 获取上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接 |
片段表达式 | ~{…} | jsp:include 作用,引入公共页面片段 |
字面量
文本值: one text,Another one! …
数字: 0 ,34 ,3.0,12.3 …
布尔值: true,false
空值: null
变量: one,two,… 变量不能有空格
文本操作
字符串拼接: +
变量替换: |The name is ${name}|
数学运算
运算符: and ,or
一元运算符: !,not
比较运算
比较: >,<,>=,<=(gt,lt,ge,le)
等式: ==,!=(eq,ne)
条件运算
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
特殊操作
无操作: _
设置属性值 -th:attr
设置单个值
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
fieldset>
form>
设置多个值
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
以上两个的代替写法
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">
所有 h5 的兼容标签写法
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes
迭代
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onionstd>
<td th:text="${prod.price}">2.41td>
<td th:text="${prod.inStock}? #{true} : #{false}">yestd>
tr>
条件运算
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">viewa>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administratorp>
<p th:case="#{roles.manager}">User is a managerp>
<p th:case="*">User is some other thingp>
div>
引入 Starter
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
只要引入了 Thymeleaf 的 Starter,SpringBoot 就已经自动配置好 Thymeleaf
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({
TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({
WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {
}
自动配置好的策略
1、所有 thymeleaf 的配置值都在 ThymeleafProperties
2、配置好了 SpringTemplateEngine
3、配好了 ThymeleafViewResolver
4、我们只需要直接开发页面
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html"; //xxx.html
拉取尚硅谷的 gitee 开源项目
首先初始化一个本地仓库
git init
克隆远程仓库的项目到刚刚初始化的本地仓库
git clone https://gitee.com/leifengyang/springboot2.git
package com.dhj.admindemo.controller;
import com.dhj.admindemo.bean.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import javax.servlet.http.HttpSession;
@Slf4j
@Controller
public class IndexController {
// 访问 / 或 login 都转发到登录页
@GetMapping({
"/", "/login"})
public String loginPage() {
return "login";
}
// 登录来到主页面,使用 post 的表单请求,与 get 类型的 /login 区分
@PostMapping("/login")
//表单中的 name 属性值与 User 的属性名一样,SpringMVC 会自动将请求的参数封装为 User 对象
public String main(User user, HttpSession session, Model model) {
// 用户名和密码不为空且密码为 111111
if (!"".equals(user.getUserName()) &&
!("".equals(user.getPassword())) &&
"111111".equals(user.getPassword())) {
// 保存登录成功的用户
session.setAttribute("loginUser", user);
/*
使用重定向到 main 页面,防止 login 表单请求的重复提交
重定向复习: 服务器向客户端返回新的请求的完整 url,客户端根据这个 url 再次发起请求
前提是该请求必须存在,否则报 404
*/
return "redirect:main.html";
}
// 否则回到登录页
log.info("用户密码错误");
model.addAttribute("msg", "账号密码错误!");
return "login";
}
/*
这里的 main.html 仅仅是请求的名叫 /main.html
模板引擎中,templates 下的所有页面,都只能通过请求解析来实现访问,除非是 static 下的静态资源,则可以直接通过 /资源名 来访问
上面的 login 表单请求最终发起的是重定向请求 /main.html ,在下面的目标方法中,则刚好处理此 /main.html 请求,
之所以加上 .html 后缀是为了方便约定,识别此种类型的请求是作为重定向跳转页面来使用的
再次刷新页面时,发送的就是 get 类型的 /main.html 请求,解决了表单重复提交的问题
*/
@GetMapping("/main.html")
public String mianPage(HttpSession session,Model model) {
// 当 session 中有用户登录时,才进入 main 页面,解决用户不登录就直接访问 main 页面的问题
if (session.getAttribute("loginUser") != null) {
// 去 main 页面
return "main";
}
log.info("用户未登录!");
model.addAttribute("msg", "用户未登录");
return "login";
}
}
通过引入 thymeleaf 来使用 thymeleaf 的公共抽取
xmlns:th="http://www.thymeleaf.org"
公共部分定义
th:fragment="公共部分的标识"
使用公共部分
th:insert="公共页面名 :: 公共部分的标识" #会将公共部分的全部内容替换到引用位置且引用位置的标签不受影响
th:replace="公共页面名 :: 公共部分的标识" #会将公共部分的全部内容替换到引用位置且引用位置的标签也会被替换
th:include="公共页面名 :: 公共部分的标识" #只取公共部分的内容进行替换,公共部分所在的外部容器标签不会被取用,例如 div 标签
以上三种方式,通过 link 标签可以引用,例如: <link th:include="">
目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
任何目标方法执行完成以后都会返回 ModelAndView(=数据和视图地址=)
processDispatchResult 处理派发结果(页面改如何响应)
视图解析:
通过自定义的拦截器,拦截未登录的访问请求
package com.dhj.admindemo.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/*
使用拦截器做登录检查
1.配置好拦截器要拦截那些请求
2.把这些配置放入容器中
*/
public class LoginInterceptor implements HandlerInterceptor {
// 目标方法执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 登录检查逻辑,判断 session 中是否有登录用户
if (request.getSession().getAttribute("loginUser") != null) {
return true; // 放行通过,继续调用目标方法方法
}
// 说明未登录,跳转到登录页
request.setAttribute("msg", "请先登录!");
request.getRequestDispatcher("/login").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 {
}
}
到配置类注册拦截器
package com.dhj.admindemo.config;
import com.dhj.admindemo.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class AdminConfig implements WebMvcConfigurer {
// 添加自定义的拦截器到容器中
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册一个自定义的拦截器 LoginInterceptor
registry.addInterceptor(new LoginInterceptor()).
// 拦截 /** 所有请求
addPathPatterns("/**").
// 过滤掉 / 和 /login 请求以及带有 /css /js 的静态资源请求不拦截 不拦截
excludePathPatterns("/", "/login", "/css/**", "/js/**", "/fonts/**", "/images/**", "/favicon.ico");
}
}
1、根据当前请求,找到 HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】
2、先来顺序执行 所有拦截器的 preHandle方法
- 如果当前拦截器 prehandler 返回为 true。则执行下一个拦截器的 preHandle
- 如果当前拦截器返回为 false。直接倒序执行所有已经执行了的拦截器的 afterCompletion;
3、如果任何一个 prehandler 拦截器返回 false。直接跳出不执行目标方法
4、所有 prehandler 拦截器都返回 True。执行目标方法
5、倒序执行所有拦截器的 postHandle 方法。
6、前面的步骤有任何异常都会直接倒序触发 afterCompletion
7、页面成功渲染完成以后,也会倒序触发 afterCompletion
package com.dhj.admindemo.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
@Slf4j
@Controller
public class FileUpload {
// 页面跳转
@GetMapping("/form_layouts")
public String form_layouts() {
return "form/form_layouts";
}
/*
文件上传请求
@RequestPart 使用该注解获取文件上传的参数
MultipartFile 使用该类自动封装上传过来的文件
*/
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("password") String password,
@RequestPart("headimage") MultipartFile headimage,
@RequestPart("photos") MultipartFile[] photos
) {
try {
// 使用 log.info 打印上传的信息,可以使用 {} 占位符来指定输出的格式
log.info("上传信息:email={},passsword={},headimage={},photos={}", email, password,
headimage.getSize(), photos.length);
// 判空
if (!headimage.isEmpty()) {
/*
transferTo()
保存文件到服务器指定磁盘路径或者 oss 服务器(阿里云对象云存储服务器)
*/
// 获取原始文件名
String name = headimage.getOriginalFilename();
headimage.transferTo(new File("D:\\Java文档资料\\后台管理系统静态资源\\springboot文件上传管理\\" + name));
}
if (photos.length > 0) {
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
String name = photo.getOriginalFilename();
photo.transferTo(new File("D:\\Java文档资料\\后台管理系统静态资源\\springboot文件上传管理\\" + name));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return "main";
}
}
表单代码
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
SpringBoot 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
原理步骤
- 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
- 2、参数解析器来解析请求中的文件内容封装成 MultipartFile
- 3、将 request 中文件信息封装为一个 Map;MultiValueMap
FileCopyUtils,实现文件流的拷贝
通过在 templates 文件夹下创建 error 目录,放入 4xx.html 和 5xx.html 便可自定义显示所有的 400 系列响应码和 500 系类响应码
注意,若配置了拦截器,则需要过滤 /error 目录下的静态资源
还可以在 html 页面中,自定义下显示那些异常信息,通过 messages 和 trace 来显示异常信息和堆栈跟踪
自定义错误页
error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确
没有就找 4xx.html
如果都没有就触发白页
@ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的,该类专门处理 @ExceptionHandler 标注的方法(推荐使用此种方式来处理全局异常)
package com.dhj.admindemo.exception;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
/*
全局的异常处理,处理整个 web 的异常
@ControllerAdvice 会自动将修饰的类放入容器
*/
@ControllerAdvice
public class GlobalExceptionHandler {
// 用于处理数学异常
/*
该注解用于标注一个异常处理器,注解的参数值,代表该异常处理器能够处理那些异常
参数类型为数组
*/
@ExceptionHandler({
ArithmeticException.class, NullPointerException.class})
public String handlerMathException(
// 参数中传入 Exception ,被该异常处理去捕获到的异常,会被自动封装到参数中的 Exception 中
Exception e) {
// // ModelAndView 使用演示
// ModelAndView modelAndView = new ModelAndView();
//
// modelAndView.addObject("", "");
// modelAndView.setViewName("");
/*
通过源码分析,发现,整个的异常处理,最终返回的都是一个 ModelAndView ,为了方便,这里直接返回视图地址
*/
return "login";//视图地址
}
}
- @ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把 responsestatus 注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error
```java
package com.dhj.admindemo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
/*
SpringMVC 支持解析加上了 @ResponseStatus 注解的自定义异常
自定义一个用户过多的异常
*/
/*
@ResponseStatus 该注解能够规定响应的状态码,以及响应的原因 reason
*/
@ResponseStatus(value = HttpStatus.FORBIDDEN,reason = "用户数量太多")
public class UserTooManyException extends RuntimeException{
public UserTooManyException(String message){
super(message);//调用父类 RuntimeException 的构造方法,传入异常信息 message
}
public UserTooManyException(){
}
}
Spring底层的异常,如参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
package com.dhj.admindemo.exception;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/*
自定义异常处理解析器
*/
@Order(value = Ordered.HIGHEST_PRECEDENCE) //定义成最高优先级,数字越小,优先级越高
@Component //需要将自定义的异常解析器放入容器中
public class CustomerHandlerResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) {
// 可以直接通过 sendError 方法来响应错误
try {
httpServletResponse.sendError(500,"服务器内部错误!");
} catch (IOException ex) {
ex.printStackTrace();
}
return new ModelAndView();
}
}
ErrorViewResolver 实现自定义处理异常;
response.sendError ,error 请求就会转给 controller
你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
basicErrorController 要去的页面地址是 ErrorViewResolver;
ErrorMvcAutoConfiguration 自动配置异常处理规则
容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
public class DefaultErrorAttributes implements ErrorAttributes**, **HandlerExceptionResolver
DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
容器中的组件:类型:BasicErrorController --> id:basicErrorController(json+白页 适配响应)
处理默认 /error 路径的请求;页面响应 new ModelAndView(“error”, model);
容器中有组件 View -> id是error;(响应默认错误页)
容器中放组件 **BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。
容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
error/404、5xx.html
如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)
写出去json
错误页
1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException
2、进入视图解析流程(页面渲染?)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
3、mv = processHandlerException;处理handler发生的异常,处理完成返回ModelAndView;
1、遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【=HandlerExceptionResolver处理器异常解析器=】
2、系统默认的 异常解析器;
1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
2、默认没有任何人能处理异常,所以异常会被抛出
1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
4、模板引擎最终响应这个页面 error/500.html
若配置了原生的 Servlet ,需要使用如下注解来扫描该 Servlet 使其正常访问,且包下的 Web 原生组件标注了相关注解,例如: @WebServlet,@WebFilter 等
//在 SpringBoot 启动类上,指定扫描原生 servlet 组件所在的包,不写,则默认扫描 BootApplication 所在的包及其下面的子包
@ServletComponentScan("com.dhj.admindemo")
推荐如上方式
DispatchServlet 如何注册进来
ServletRegistrationBean
把 DispatcherServlet 配置进来。Tomcat-Servlet;
多个Servlet都能处理到同一层路径,精确优选原则
使用 RegistrationBean
@Configuration
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my","/my02");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
// return new FilterRegistrationBean(myFilter,myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}
默认支持的webServer
Tomcat
, Jetty
, or Undertow
ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
切换服务器
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
exclusion>
exclusions>
dependency>
原理
ServletWebServerApplicationContext
ServletWebServerApplicationContext
启动的时候寻找 ServletWebServerFactory``(Servlet 的web服务器工厂---> Servlet 的web服务器)
TomcatServletWebServerFactory
, JettyServletWebServerFactory
, or UndertowServletWebServerFactory
底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize---this.tomcat.start();
内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
``
实现 WebServerFactoryCustomizer
ServletWebServerFactory 进行绑定
修改配置文件 server.xxx
直接自定义 ConfigurableServletWebServerFactory
xxxxxCustomizer: 定制化器,可以改变xxxx的默认规则
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;
@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}
@Configuration
public class AdminWebConfig implements WebMvcConfigurer
@EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能
… …
场景 starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jdbcartifactId>
dependency>
此时,数据库驱动未自动导入,因为 SpringBoot 不知道我们要操作什么数据库,需手动导入
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.8version>
dependency>
也可以不写版本,SpringBoot 自动版本仲裁,需要自定义版本,可以如上述代码,直接声明版本,或者如下,在 properties 中声明统一版本
<properties>
<java.version>1.8java.version>
<mysql.version>5.1.43mysql.version>
properties>
自动配置的类
DataSourceAutoConfiguration : 数据源的自动配置
@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
通过配置文件配置数据源
spring:
datasource: #配置数据源
url: #配置数据库连接的 url
jdbc:mysql://localhost:3306/user_db
username: root #配置用户名
password: dhj461212 #配置密码
#type: com.zaxxer.hikari.HikariDataSource #配置数据源类型,可以不用配置,SpringBoot 默认配置
driver-class-name: com.mysql.jdbc.Driver #配置数据库驱动
启动 SpringBoot 应用程序,若出现一下警告
WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification
则修改 url 为如下即可
# 代表该连接支持多重查询,设置时区为东八区,不使用 SSL
jdbc:mysql://localhost:3306/user_db?allowMultiQueries=true&serverTimezone=GMT%2B8&useSSL=false
测试
package com.dhj.admindemo;
import com.dhj.admindemo.boot.AdminDemoApplication;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@SpringBootTest()
class AdminDemoApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
Long aLong = jdbcTemplate.queryForObject("select count(*) from t_user", Long.class);
log.info("记录数:" + aLong);
}
}
测试时,可能报如下错
you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test
那是因为没有在 @SpringBootTest 中配置 SpringBoot 启动类的的 classes 配置,修改为如下配置即可
@SpringBootTest(classes = AdminDemoApplication.class)
driud 官方 github 地址: https://github.com/alibaba/druid)
引入依赖
<dependency> <groupId>com.alibabagroupId> <artifactId>druidartifactId> <version>1.1.17version> dependency>
通过配置类,配置 druid 数据源的各种配置
package com.dhj.admindemo.config; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import javax.sql.DataSource; import java.sql.SQLException; import java.util.Arrays; @Configuration public class MyDruidDataSource { /* 返回数据源的统一接口 DataSource SpringBoot 数据源的默认配置逻辑为,容器中没有数据源,才会默认配置 这里我们自己定义了数据源到容器中,则被 SpringBoot 优先使用 */ @ConfigurationProperties("spring.datasource") //将该组件中的属性与配置文件中的 spring.datasource 进行1绑定 @Bean public DataSource dataSource() { // druidDataSource.setUrl("jdbc:mysql://localhost:3306/user_db?allowMultiQueries=true&serverTimezone=GMT%2B8&useSSL=false"); // druidDataSource.setName("root"); // druidDataSource.setPassword("dhj461212"); DruidDataSource druidDataSource = new DruidDataSource(); // Druid 内置一个 StatFilter 用于统计监控信息,提供如下方式,加入监控功能 try { // 设置数据源的 Filters 属性为 stat ,便可开启 druid 监控功能 // 还可以添加 SQL 防火墙功能 wall,在 setFilters() 可以添加多个配置值,写在一个字符串中,使用逗号分隔即可 druidDataSource.setFilters("stat,wall"); } catch (SQLException e) { e.printStackTrace(); } return druidDataSource; } /* 配置 druid 监控页的功能 将 druid 提供的 StatViewServlet 组件使用 ServletRegistrationBean 注册到容器中 */ @Bean public ServletRegistrationBean<StatViewServlet> servletRegistrationBean() { // 创建 StatViewServlet 实例 StatViewServlet statViewServlet = new StatViewServlet(); /* 注册 StatViewServlet ,配置处理请求的路径为: /druid/* , 补充,在 SpringBoot 中,拦截所有请求的写法为 /** 原生 Web 中拦截所有请求的写法为 /* */ ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*"); // 设置 druid 监控页查看的用户名和密码 registrationBean.addInitParameter("loginUsername", "zhangsan"); registrationBean.addInitParameter("loginPassword", "111111"); return registrationBean; } /* WebStatFilter 用于采集 web-jdbc 关连监控的数据\ 将 WebStatFilter 注册进容器 */ @Bean public FilterRegistrationBean<WebStatFilter> filterRegistrationBean() { WebStatFilter webStatFilter = new WebStatFilter(); // 创建 WebStatFilter 实例 //通过 FilterRegistrationBean 注册 WebStatFilter FilterRegistrationBean<WebStatFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>(webStatFilter); // 设置 filterFilterRegistrationBean 的拦截参数,这里设置的是 filter 的拦截路径 filterFilterRegistrationBean.setUrlPatterns(Arrays.asList("/*")); /* 通过初始参数的方式,设置 filter 过滤掉的拦截路径,若以前 xml 的配置形式中,有配置 init 参数时,在 SpringBoot 中,就可以使用 addInitParameter() 的方式来写,包括一些需要在 xml 中配置的属性,则可以使用 setXXX 来写 */ filterFilterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterFilterRegistrationBean; } }
或者直接使用 yaml 配置文件的方式来配置
spring: datasource: url: jdbc:mysql://localhost:3306/db_account username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver druid: aop-patterns: com.atguigu.admin.* #监控 SpringBean filters: stat,wall #底层开启功能,stat(sql监控),wall(防火墙) stat-view-servlet: #配置监控页功能 enabled: true login-username: admin login-password: admin 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: enabled: true config: drop-table-allow: false
详细可参考官方文档
引入 starter
<dependency> <groupId>com.alibabagroupId> <artifactId>druid-spring-boot-starterartifactId> <version>1.1.17version> dependency>
在配置文件中配置基本配置项,配置一些数据源信息
spring.datasource.url=jdbc:mysql://localhost:3306/user_db?allowMultiQueries=true&serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf8 spring.datasource.username=root spring.datasource.password=dhj461212
配置好后,因为是以 starter 方式配置的 druid,因此,SpringBoot 会自动配置 druid 的相关配置,具体如下
- 使用的是扩展配置项 spring.datasource.druid 来配置的
- DruidSpringAopConfiguration.class, 监控SpringBean的;配置项:spring.datasource.druid.aop-patterns
- DruidStatViewServletConfiguration.class, 监控页的配置:spring.datasource.druid.stat-view-servlet;默认开启
- DruidWebStatFilterConfiguration.class, web监控配置;spring.datasource.druid.web-stat-filter;默认开启
- DruidFilterConfiguration.class}) 所有Druid自己filter的配置
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat"; private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config"; private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding"; private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j"; private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j"; private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2"; private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log"; private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
若需要配置相关功能,都可以通过配置文件来修改,例如
spring: #配置 druid 数据源 druid: aop-patterns: com.atguigu.admin.* #监控 SpringBean filters: stat,wall #底层开启功能,stat(sql监控),wall(防火墙) stat-view-servlet: #配置监控页功能 enabled: true login-username: admin login-password: admin 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: enabled: true config: drop-table-allow: false
配置文件小总结
jdbc 配置
spring.datasource.druid.url= # 或spring.datasource.url= spring.datasource.druid.username= # 或spring.datasource.username= spring.datasource.druid.password= # 或spring.datasource.password= spring.datasource.druid.driver-class-name= #或 spring.datasource.driver-class-name=
连接池配置
spring.datasource.druid.initial-size= spring.datasource.druid.max-active= spring.datasource.druid.min-idle= spring.datasource.druid.max-wait= spring.datasource.druid.pool-prepared-statements= spring.datasource.druid.max-pool-prepared-statement-per-connection-size= spring.datasource.druid.max-open-prepared-statements= #和上面的等价 spring.datasource.druid.validation-query= spring.datasource.druid.validation-query-timeout= spring.datasource.druid.test-on-borrow= spring.datasource.druid.test-on-return= spring.datasource.druid.test-while-idle= spring.datasource.druid.time-between-eviction-runs-millis= spring.datasource.druid.min-evictable-idle-time-millis= spring.datasource.druid.max-evictable-idle-time-millis= spring.datasource.druid.filters= #配置多个英文逗号分隔 ....//more
监控配置
# WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter spring.datasource.druid.web-stat-filter.enabled= #是否启用StatFilter默认值false spring.datasource.druid.web-stat-filter.url-pattern= spring.datasource.druid.web-stat-filter.exclusions= spring.datasource.druid.web-stat-filter.session-stat-enable= spring.datasource.druid.web-stat-filter.session-stat-max-count= spring.datasource.druid.web-stat-filter.principal-session-name= spring.datasource.druid.web-stat-filter.principal-cookie-name= spring.datasource.druid.web-stat-filter.profile-enable= # StatViewServlet配置,说明请参考Druid Wiki,配置_StatViewServlet配置 spring.datasource.druid.stat-view-servlet.enabled= #是否启用StatViewServlet(监控页面)默认值为false(考虑到安全问题默认并未启动,如需启用建议设置密码或白名单以保障安全) spring.datasource.druid.stat-view-servlet.url-pattern= spring.datasource.druid.stat-view-servlet.reset-enable= spring.datasource.druid.stat-view-servlet.login-username= spring.datasource.druid.stat-view-servlet.login-password= spring.datasource.druid.stat-view-servlet.allow= spring.datasource.druid.stat-view-servlet.deny= # Spring监控配置,说明请参考Druid Github Wiki,配置_Druid和Spring关联监控配置 spring.datasource.druid.aop-patterns= # Spring监控AOP切入点,如x.y.z.service.*,配置多个英文逗号分隔
Druid Spring Boot Starter 不仅限于对以上配置属性提供支持,
DruidDataSource
内提供setter
方法的可配置属性都将被支持。你可以参考 WIKI 文档或通过 IDE 输入提示来进行配置。配置文件的格式你可以选择.properties
或.yml
,效果是一样的,在配置较多的情况下推荐使用.yml
。SpringBoot 配置示例
https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter配置项列表
https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8
引入 starter
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.4version>
dependency>
MyBatis 的配置
全局配置文件
SqlSessionFactory: 自动配置好了
SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession
@Import(AutoConfiguredMapperScannerRegistrar.class);会扫描标注了 @Mapper 注解的 SQL 映射接口
Mapper: 只要我们写的操作 MyBatis 的接口标注了 @Mapper 就会被自动扫描进来
1.配置 mybatis 全局配置文件
<configuration> configuration>
2.SQL 映射配置文件的编写和 MyBatis 一般操作无异
<mapper namespace="com.dhj.admindemo.dao.PersonMapper"> <select id="getPersonById" resultType="com.dhj.admindemo.bean.Person"> select * from t_user where id=#{id} select> mapper>
3.SQL 映射的接口类上需要标注 @Mapper 注解
4.注意: 数据源要提前配置好,配置过的数据源,SprngBoot 会为 MyBatis 自动配置该数据源
5.在 SpringBoot 配置文件中配置 MyBatis 的相关配置文件
mybatis: config-location: classpath:mybatis/mybatis-config.xml #指定 mybatis 全局配置文件的位置 mapper-locations: classpath:mybatis/mapper/*.xml #指定 sql 映射文件的位置
补充:
全局配置文件也可以不用创建,根据个人习惯,若不创建,则在配置文件中通过 mybatis.configuration 前缀,便相当于修改全局配置文件中的值,如下# 配置mybatis规则 mybatis: # config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mybatis/mapper/*.xml configuration: map-underscore-to-camel-case: true #开启驼峰命名 可以不写全局配置文件,所有全局配置文件的配置都放在configuration配置项中即可
配置文件的方式配置 MyBatis 的总结
引入 starter
实现需要自定义的配置数据源,否则使用 SpringBoot 默认的 HikariDataSource
配置 MyBatis 全局配置文件,也可以不写,通过 SpringBoot 配置文件来设置其中配置项
编写 SQL 映射接口,标注 @Mapper 注解
编写 SQL 映射配置文件,在 namespace 与对应的 SQL 映射接口绑定
在 SpringBoot 配置文件中编写基本的配置信息
# 配置mybatis规则 mybatis: # config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mybatis/mapper/*.xml configuration: map-underscore-to-camel-case: true #开启驼峰命名,若数据库字段名与 JavaBean 属性名一样则无需考虑驼峰的字段映射 可以不写全局;配置文件,所有全局配置文件的配置都放在configuration配置项中即可 注:若将全局配置文件的定义写到 SpringBoot 配置文件中,则上面的 config-location 就不要声明,二者不能同时存在,否则引起冲突
启动项目,此时可能会遇到如下类似报错
Field personMapper in com.dhj.admindemo.service.PersonService required a bean of type 'com.dhj.admindemo.dao.PersonMapper' that could not be found.
首先检查 SQL 映射接口上是否添加了 @Mapper 注解,若没有,则添加上
然后检查 SpringBott 启动类的目录等级是否在最外层包下,也就是所有代码的最高父级包,因为 SpringBoot 默认扫描的是启动类所在的包以及子包,将启动类移动到外层包即可
或者添加
MapperScan("包路径")
来指定扫描 SQL 映射接口所在的包
package com.dhj.admindemo.dao; import com.dhj.admindemo.bean.City; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Options; import org.apache.ibatis.annotations.Select; @Mapper public interface CityMapper { // 通过注解来使用 SQL @Select("select * from city where id = #{id}") // 对于 SQL 映射文件中的属性,可以使用 @Options 来设置 @Options(useGeneratedKeys = true, keyProperty = "id") City getCityById(Integer id); // 若不使用注解,则需要配置 SQL 映射文件 void insertCity(City city); }
引入mybatis-starter
配置application.yaml中,指定 mapper-location 位置即可
编写 Mapper 接口并标注@Mapper 注解
简单方法直接注解方式
复杂方法编写 mapper.xml 进行绑定映射
@MapperScan(“com.atguigu.admin.mapper”) 简化,其他的接口就可以不用标注 @Mapper注解,只是简化写法,建议每个标注
MyBatis-Plus (https://github.com/baomidou/mybatis-plus)(简称 MP)是一个 MyBatis (http://www.mybatis.org/mybatis-3/) 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
mybatis plus 官网(https://baomidou.com/)
建议安装 MybatisX 插件
引入依赖
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.1version>
dependency>
MybatisPlusAutoConfiguration 配置类与 MybatisPlusProperties 配置项绑定
在 SpringBoot 配置文件中,mybatis-plus: xxx 就是对 mybatis-plus 的定制
SqlSessionFactory 自动配置好。底层是容器中默认的数据源
mapperLocations ( SQL 映射的 xml 文件 ) 也是自动配置好的。有默认值。classpath*:/mapper/**/*.xml;任意包的类路径下的所有 mapper 文件夹下任意路径下的所有 xml 都会自动扫描为 sql 映射文件。 建议以后sql映射文件,放在classpath:mapper 目录下
容器中也自动配置好了 SqlSessionTemplate
@Mapper 标注的接口也会被自动扫描;或者直接 @MapperScan(“com.atguigu.admin.mapper”) 批量扫描就行
使用 MyBatis-plus 后,SpringBoot 中的 SQL 映射文件的地址也无需在配置了
根据数据库中刚刚创建的表,创建 JavaBean
package com.dhj.admindemo.bean; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; @Data @AllArgsConstructor @NoArgsConstructor @ToString /* 默认情况下,mybatis-plus 认为 JavaBean 对象的类名就和表名相关,默认查询该表 若数据库中没有该表,则可以通过 @TableName("user") 来指定一张表名为 user 的表 */ @TableName("user") public class User2 { /* 在mybatis-plus中,原则上,所有属性,都应该在数据库中存在对应字段,但是不方便, 可以使用 @TableField(exist = false) 在某一个属性上标注其在数据库中不存在 */ @TableField(exist = false) private String gender; private Long id; private String name; private Integer age; private String email; }
编写一个 Mapper 接口,继承 MyBatis-Plus 提供的 BaseMapper 接口,传入泛型参数,参数为需要操作的数据库表对应的 JavaBean 实体类,便可获得该实体类所有的 CRUD 功能而不用编写 SQL 映射的 xml 文件
@Mapper public interface User2Mapper extends BaseMapper<User2> { }
使用
@Autowired User2Mapper user2Mapper; 注入 Mapper User2 user2 = user2Mapper.selectById(1); // 直接提供 Mapper 调用继承的 BaseMapper 中的方法即可 System.out.println(user2)
若觉得 MyBatis-Plus 提供的 CRUD 方法不满足需求,也可以编写 xml 映射文件来与 Mapper 接口绑定使用
<mapper namespace="com.dhj.admindemo.dao.User2Mapper"> mapper>
一般的 CRUD 功能,由 client 发送业务逻辑请求
controller 接收业务逻辑请求
相关的业务逻辑请求通过对应的 Service 接口来定义例如查询用户,注册用户,用户登录等
Service 接口只负责定义,具体业务逻辑的实现,由对应的 ServiceImpl 实现类实现 Service 接口在实现类中来编写,且需要将其加入 Spring 容器中
在 Service 实现类编写的业务中,若涉及到了数据层的操作,就需要调用 Mapper 层的接口来访问操作数据库
controller 只需要通过 @Autowired 注入 Service 接口,调用其中的业务逻辑即可,因为接口的实现类已经加入到了 Spring 容器中
以下进行代码演示
首先,定义 Service 接口
package com.dhj.admindemo.service; import com.baomidou.mybatisplus.extension.service.IService; import com.dhj.admindemo.bean.User2; /* User2Service 接口,在接口中定义业务逻辑 IService 是 MyBatis-Plus 提供的顶级接口,其中定义了很多常用的业务逻辑相关的数据层操作方法 通过泛型规定对哪一个数据类型进行接口中定义了的操作 */ public interface User2Service extends IService<User2> { }
定义接口实现类
package com.dhj.admindemo.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.dhj.admindemo.bean.User2; import com.dhj.admindemo.dao.User2Mapper; import com.dhj.admindemo.service.User2Service; import org.springframework.stereotype.Service; /* User2Service 的实现类 User2ServiceImpl,对 User2Service 中定义业务逻辑进行具体的实现 由 Controller 中,涉及到相关业务逻辑的请求来调用 因为对应的 Service 实现类已经实现了 IService 接口,其中定义的许多方法不可能在 ServiceImpl 中全部实现, 因此,MyBatis-Plus 提供了一个 ServiceImpl 类,该类实现了 IService 中的方法,我们自己的 ServiceImpl 只需要继承 ServiceImpl 即可 ServiceImpl 类有两个泛型参数,分别为 1.当前 Service 实现类需要使用到的 Mapper 接口 2.当前 Service 实现类需要操作的数据类型 */ @Service public class User2ServiceImpl extends ServiceImpl<User2Mapper, User2> implements User2Service { }
定义 Mapper
package com.dhj.admindemo.dao; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.dhj.admindemo.bean.User2; import org.apache.ibatis.annotations.Mapper; /* User2Mapper 数据访问层,由 User2ServiceImpl 中,涉及到数据访问的业务逻辑来调用 该 User2Mapper 继承了一个 BaseMapper 接口,BaseMapper 接口中定义了许多操作数据层的方法 */ @Mapper public interface User2Mapper extends BaseMapper<User2> { }
提供 MyBatis-Plus 提供的 API 来实现分页
// 分页查询演示 @ResponseBody @GetMapping("/dynamic_table_table") public List<User2> getUserPage( // 通过参数传入页码,默认值 1 @RequestParam(value = "p", defaultValue = "1") Integer p) { /* MyBatis-Plus 提供的 Page 对象,参数分别为, 指定的页码 p 每页记录数 2 条 */ Page<User2> user2Page = new Page<>(p, 2); /* page(): MyBatis-Plus 提供的接口中的分页方法,该方法有最多两个参数,分别为 1.一个 page 对象,包含指定页码的全部信息 2.一个 Wapper 对象(实体类的条件封装器,可以理解为在封装实体类时,按照指定条件对多个实体类进行符合条件的筛选) 可以不填该参数 */ Page<User2> page = user2Service.page(user2Page); page.getCurrent();// 获取当前页码 page.getTotal();// 获取总记录 page.getPages();// 获取总页码 page.getSize();// 显示每页的条数 List<User2> records = page.getRecords();// 获取当前页数据 return records; }
还有将分页插件添加到容器
package com.dhj.admindemo.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyBatisConfig { // 向容器中添加分页插件 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { // MyBatis-Plus 拦截器 MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); /* 创建 PaginationInnerInterceptor 分页拦截器的实例, 添加到 MyBatis-Plus 拦截器 MybatisPlusInterceptor 中 */ interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } }
引入 stater
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
自动配置
自动配置:
- RedisAutoConfiguration 自动配置类。RedisProperties 属性类 --> spring.redis.xxx是对 redis 的配置
- 连接工厂是准备好的,通过 LettuceConnectionConfiguration、JedisConnectionConfiguration 来分别为 Lettuce 和 Jedis 配置 xxxxConnectionFactory,最终都会被注册到 RedisConnectionFactory 中
- 自动注入了 xxxTemplate 来操作 Redis
- 自动注入了 RedisTemplate
- 自动注入了 StringRedisTemplate;k : v 都是String
- 底层只要我们使用 StringRedisTemplate、RedisTemplate 就可以操作 redis
阿里云 Redis 环境搭建
1、购买阿里云按量付费 redis。经典网络
2、申请 redis 的公网连接地址
3、修改白名单 允许 0.0.0.0/0 所有 ip 访问
使用 Redis 客户端连接 Redis 即可
通过配置文件,设置连接 Redis 的 url
# 配置 redis 相关
# 配置连接的 url,格式为 redis://用户名:密码@ip地址:端口号
spring.redis.url=redis://redis:[email protected]:6379
# 还有其他配置,可参考文档
因为 redis 的 stater 已经自动引入了 RedisTemplate 和 StringRedisTemplate 并放入容器中,通过 @Autowride 便可自动注入使用
@Test
public void testRedis() {
// redis 字符串类型的操作
ValueOperations<String, String> stringStringValueOperations = stringRedisTemplate.opsForValue();
stringStringValueOperations.set("str", "hello");
String str = stringStringValueOperations.get("str");
System.out.println(str);
}
此时测试连接,可能会出现如下类似异常
Unable to connect to Redis; nested exception
原因在于使用 url 的方式连接阿里云的 redis 会有问题,这里不深究,在配置文件中使用默认的 host 方式来连接即可
#以下使用 host 的方式来连接 redis
#主机地址
spring.redis.host=r-bp1058aayw86al1jywpd.redis.rds.aliyuncs.com
#如果使用自定义账号连接 redis 实例,密码格式为 账号:密码
spring.redis.password=redis:Dhj4612_
spring.redis.port=6379
引入 Jedis 客户端依赖,支持版本自动仲裁
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
在配置文件中申明 Redis 的客户端类型为 Jedis
#声明客户端类型为 jedis
spring.redis.client-type=jedis
使用 Jedis 的客户端后,底层容器中放入的 Redis 连接工厂就为 Jedis 的连接工厂 JedisConnectionFactory
// 通过 RedisConnectionFactory的 class 可以查看当前的连接工厂是谁的,用以查看 redis 当前使用的客户端
System.out.println(connectionFactory.getClass());
使用自定义的拦截器实现
package com.dhj.admindemo.interceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /* 自定义使用 redis 统计 url 请求次数的拦截器 */ @Component public class RedisUrlCountIncterceptor implements HandlerInterceptor { // 自动注入 StringRedisTemplate @Autowired StringRedisTemplate stringRedisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取请求的 uri String requestURI = request.getRequestURI(); // uri 每次访问,则以该 uri 为 key 计数加 1 stringRedisTemplate.opsForValue().increment(requestURI); // 打印出每次请求的 uri 次数 System.out.println(requestURI + " : " + stringRedisTemplate.opsForValue().get(requestURI)); return true; } }
注册该拦截器到 Web 配置中
package com.dhj.admindemo.config; import com.dhj.admindemo.interceptor.LoginInterceptor; import com.dhj.admindemo.interceptor.RedisUrlCountIncterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /* filter 和 interceptor 都具有相同的功能,该如何选择 1.filter 是 servlet 定义的原生组件,脱离了 spring 也能使用 2.intercepotor 是 spring 定义的接口,可以使用 spring 提供的相关功能,例如 @Autowirte 自动注入 */ @Configuration public class AdminConfig implements WebMvcConfigurer { // 自动注入自定义的 Redis 的拦截器 @Autowired RedisUrlCountIncterceptor redisUrlCountIncterceptor; // 添加自定义的拦截器到容器中 @Override public void addInterceptors(InterceptorRegistry registry) { // 注册一个自定义的拦截器 LoginInterceptor registry.addInterceptor(new LoginInterceptor()). // 拦截 /** 所有请求 addPathPatterns("/**"). // 过滤掉 / 和 /login 等指定请求不拦截 excludePathPatterns("/", "/login", "/css/**", "/js/**", "/fonts/**", "/images/**", "/favicon.ico" , "/error/**"); //=======================================如下是自定义的 redis 计数拦截器======================== /* 此处不能使用 new 的方式传入自定义的拦截器实例到 addInterceptor 方法,因为只有在容器中的组件,spring 才会解析组件中涉及到的注解,若是使用主动 new 的方式, 则 new 出的实例没有放入容器中,不能被 sprng 控制,所以,采用 @Autowirde 自动注入的方式来获取 RedisUrlCountIncterceptor 实例 并注入到 WebMvcConfigurer的拦截器配置中 */ registry.addInterceptor(redisUrlCountIncterceptor). addPathPatterns("/**"). excludePathPatterns("/", "/login", "/css/**", "/js/**", "/fonts/**", "/images/**", "/favicon.ico" , "/error/**"); } }
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
作为最新版本的JUnit框架,JUnit5 与之前版本的 Junit 框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform: Junit Platform 是在JVM上启动测试框架的基础,不仅支持 Junit 自制的测试引擎,其他测试引擎也都可以接入。
JUnit Jupiter: JUnit Jupiter 提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心。内部包含了一个测试引擎,用于在 Junit Platform上运行。
JUnit Vintage: 由于JUint 已经发展多年,为了照顾老的项目,JUnit Vintage 提供了兼容 JUnit4.x,Junit3.x 的测试引擎
注意:
SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容 junit4 需要自行引入(不能使用 junit4 的功能 @Test)
JUnit 5’s Vintage Engine Removed from
spring-boot-starter-test
,如果需要继续兼容 junit4 需要自行引入vintage
<dependency> <groupId>org.junit.vintagegroupId> <artifactId>junit-vintage-engineartifactId> <scope>testscope> <exclusions> <exclusion> <groupId>org.hamcrestgroupId> <artifactId>hamcrest-coreartifactId> exclusion> exclusions> dependency>
当前 2.4.5 版本 SpringBoot 的 junit 使用演示
引入 junit 场景依赖
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-testartifactId> <scope>testscope> dependency>
package com.dhj.admindemo; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @Slf4j @SpringBootTest(classes = AdminDemoApplication.class) class AdminDemoApplicationTests { @Test void contextLoads() { } }
SpringBoot整合Junit以后。
- 编写测试方法:@Test标注(注意需要使用 junit5 版本的注解)
- Junit 类具有 Spring 的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚
Unit5的注解与JUnit4的注解有所变化
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
简单演示
package com.dhj.admindemo;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.context.SpringBootTest;
@DisplayName("测试 Junit5")
/*
@SpringBootTest
标注该注解后,测试类便可以使用 SpringBoot 中的相关功能,例如自动注入 @Autowired
*/
@SpringBootTest
public class Junit5Demo {
/*
@DisplayName
该注解能够标注到方法和类上,对测试的方法和类标注一个名字,方便识别
*/
@DisplayName("测试 displayName ")
@Test
void testFirst() {
System.out.println("HelloWorld");
}
/*
@BeforeEach
被该注解标注的方法,在每一个单元测试方法执行之前,都会运行
*/
@BeforeEach
void beforeEach() {
System.out.println("测试开始");
}
/*
@BeforeAll
在所有测试单元开始之前执行
该方法必须申明为静态的,保证优先加载执行
*/
@BeforeAll
static void beforeAll() {
System.out.println("所有单元测试开始");
}
/*
@AfterEach
在每一个测试方法结束以后运行
*/
@AfterEach
void afterEach() {
System.out.println("测试结束");
}
/*
@AfterAll
在所有的测试方法结束之后执行
该方法必须申明为静态的,保证优先加载
*/
@AfterAll
static void afterAll() {
System.out.println("所有测试已经结束");
}
}
断言(assertions),字面理解,断定某件事情会发生,是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法
通过断言机制,能够检查业务逻辑返回的数据是否合理,所有的测试运行结束以后,会有一个详细的测试报告
**JUnit 5 内置的断言可以分成如下几个类别: **
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
@DisplayName("测试简单断言")
@Test
void SimpleAssertions() {
int sum = sum(1, 2);
/*
assertEquals 判断两个对象或两个原始类型是否相等
方法参数分别为
1.预期值
2.实际的值
3.断言失败输出的信息
*/
assertEquals(3, sum, "预期值与实际值不符");
Object o = new Object();
Object o1 = new Object();
/*
判断第二个参数对象的引用是否指向第一个参数的引用
参数分别为
1 预期值的对象
2 实际进行断言的对象
3 断言失败的输出信息
*/
assertSame(o, o1,"两个对象并非执行同一引用");
}
int sum(int i, int j) {
return i + j;
}
注意: 若前面的断言失败,后面的所有代码都不会执行
通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等
@Test
@DisplayName("array assertion")
public void array() {
/*
预期值第一个参数中的数组
实际断言的为第二个参数中的数组
根据第一个参数中的数组,与第二个参数中的数组进行比较是否相等
若数组2中的元素或者对应所有的元素与数组1中的不同,则断言失败
*/
assertArrayEquals(new int[]{
1, 2}, new int[] {
1, 2},"数组内容不相等");
}
assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言
/*
将多个要测试的断言组合到一起,有一个断言失败,则整体断言失败
参数分别为
1. 为这一组断言起一个标题
2. 多个 Executable 实现,因为 Executable 接口中定义的方法是没有参数
因此,推荐使用 lambda 快速实现
*/
@DisplayName("组合断言演示")
@Test
void testAll() {
assertAll("组合断言1",
() -> assertTrue(true && true,"结果不为 true"),
() -> assertEquals(1, 1,"断言失败")
);
}
在JUnit4时期,想要测试方法的异常情况时,需要用 @Rule 注解的 ExpectedException 变量还是比较麻烦的。而JUnit5提供了一种新的断言方式 Assertions.assertThrows(),配合函数式编程就可以进行使用。
/*
断言一定会抛出某种类型的异常
参数分别为
1. 断言一定会抛出的异常类型
2. 断言的目标对象: Executable 接口的实现
3. 断言失败的信息
*/
@DisplayName("异常断言")
@Test
void exceptionAssert() {
assertThrows(NullPointerException.class, () -> {
int[] ints = new int[10];
System.out.println(ints[10]);//ArrayIndexOutOfBoundsException
}, "断言失败");
}
设置一个超时时间
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
通过 fail 方法直接使得测试失败
@Test
@DisplayName("fail")
public void shouldFail() {
fail("This should fail");
}
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 对象才会被执行;当条件不满足时,测试执行并不会终止。
JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用 @BeforeEach 和 @AfterEach 注解,而且嵌套的层次没有限制。
package com.dhj.admindemo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.util.EmptyStackException;
import java.util.Stack;
@DisplayName("嵌套测试")
public class TestAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
// 嵌套测试的情况下,外层的 Test 不能驱动内层的 Before & After: Each | All 之类的方法
assertNull(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 可以驱动外层的 Before & After: Each | All 之类的方法
@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());
}
}
}
}
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用 @ValueSource 等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现 ArgumentsProvider 接口,任何外部文件都可以作为它的入参
@ParameterizedTest
@ValueSource(strings = {
"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}
static Stream<String> method() {
return Stream.of("apple", "banana");
}
在进行迁移的时候需要注意如下的变化:
- 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中
- 把 @Before 和 @After 替换成 @BeforeEach 和 @AfterEach
- 把 @BeforeClass 和 @AfterClass 替换成 @BeforeAll 和 @AfterAll
- 把 @Ignore 替换成 @Disabled
- 把 @Category 替换成 @Tag
- 把 @RunWith | @Rule | @ClassRule 替换成 @ExtendWith
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot 就抽取了Actuator 场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能
引入 starter 依赖
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-actuatorartifactId> dependency>
通过 http://localhost:8080/actuator/** 可以访问当前项目的指定端点,直接访问 http://localhost:8080/actuator/ 根路径,则会显示当前项目可以监控的端点,但是默认这些端点不是全部开启的,可以通过配置文件修改暴露所有端点,如下
暴露所有的监控信息为 HTTP
#所有 Actuator 的配置 management: endpoints: enabled-by-default: true #暴露所有端点信息 web: exposure: include: '*' #以web方式暴露
测试
http://localhost:8080/actuator/beans
http://localhost:8080/actuator/configprops
http://localhost:8080/actuator/metrics
http://localhost:8080/actuator/metrics/jvm.gc.pause
http://localhost:8080/actuator/端点名/端点路径
补充
由于之前配置了 Redis ,项目启动后,通过端口监控发生项目的 health 时,发现值为: DOWN(不健康状态,UP 为健康状态),此时,可以在项目中禁用 redis 的相关内容
// 禁用 redis 的自动配置 @SpringBootApplication(scanBasePackages = "com.dhj.admindemo",exclude = RedisAutoConfiguration.class) // 注释掉其余代码中引用到了 redis 的部分
ID | 描述 |
---|---|
auditevents |
暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件 。 |
beans |
显示应用程序中所有 Spring Bean 的完整列表。 |
caches |
暴露可用的缓存 |
conditions |
显示自动配置的所有条件信息,包括匹配或不匹配的原因。 |
configprops |
显示所有@ConfigurationProperties 。 |
env |
暴露Spring的属性ConfigurableEnvironment |
flyway |
显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway 组件。 |
health |
显示应用程序运行状况信息。 |
httptrace |
显示 HTTP 跟踪信息(默认情况下,最近 100 个 HTTP 请求-响应)需要一个HttpTraceRepository 组件。 |
info |
显示应用程序信息。 |
integrationgraph |
显示 Spring integrationgraph 。需要依赖spring-integration-core 。 |
loggers |
显示和修改应用程序中日志的配置。 |
liquibase |
显示已应用的所有 Liquibase 数据库迁移。需要一个或多个Liquibase 组件。 |
metrics |
显示当前应用程序的“指标”信息。 |
mappings |
显示所有@RequestMapping 路径列表。 |
scheduledtasks |
显示应用程序中的计划任务。 |
sessions |
允许从 Spring Session 支持的会话存储中检索和删除用户会话。需要使用 Spring Session 的基于 Servlet 的 Web 应用程序。 |
shutdown |
使应用程序正常关闭。默认禁用。 |
startup |
显示由ApplicationStartup 收集的启动步骤数据。需要使用SpringApplication 进行配置BufferingApplicationStartup 。 |
threaddump |
执行线程转储。 |
如果你的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点
ID | 描述 |
---|---|
heapdump |
返回hprof 堆转储文件。 |
jolokia |
通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core 。 |
logfile |
返回日志文件的内容(如果已设置logging.file.name 或logging.file.path 属性)。支持使用HTTPRange 标头来检索部分日志文件的内容。 |
prometheus |
以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus 。 |
最常用的 Endpoint
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
- health endpoint 返回的结果,应该是一系列健康检查后的一个汇总报告
- 很多的健康检查默认已经自动配置好了,比如:数据库、redis 等
- 可以很容易的添加自定义的健康检查机制
- 只有当所有涉及到的状态都正常,则 health 正常
配置 health
# 所有 Actuator 的配置 management: endpoints: enabled-by-default: true #暴露所有端点信息 web: exposure: include: '*' #以web方式暴露 endpoint: # management.endpoint.端点名.xxx 配置某一个具体端点的监控 health: show-details: always #总是显示端口的详细信息
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;
- 通过Metrics对接多种监控系统
- 简化核心Metrics开发
- 添加自定义Metrics或者扩展已有Metrics
默认所有的 Endpoint除过 shutdown 都是开启的。
需要开启或者禁用某个Endpoint。配置模式为 management.endpoint..enabled = true
management: endpoint: beans: enabled: true
或者禁用所有的 Endpoint 然后手动开启指定的 Endpoint
management: endpoints: enabled-by-default: false endpoint: beans: enabled: true health: enabled: true
支持暴露的方式
- HTTP: 默认只暴露 health 和 info 端点
- JMX: 默认暴露所有 Endpoin
- 除过 health 和 info,剩下的 Endpoint 都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则
ID | JMX | Web |
---|---|---|
auditevents |
Yes | No |
beans |
Yes | No |
caches |
Yes | No |
conditions |
Yes | No |
configprops |
Yes | No |
env |
Yes | No |
flyway |
Yes | No |
health |
Yes | Yes |
heapdump |
N/A | No |
httptrace |
Yes | No |
info |
Yes | Yes |
integrationgraph |
Yes | No |
jolokia |
N/A | No |
logfile |
N/A | No |
loggers |
Yes | No |
liquibase |
Yes | No |
metrics |
Yes | No |
mappings |
Yes | No |
prometheus |
N/A | No |
scheduledtasks |
Yes | No |
sessions |
Yes | No |
shutdown |
Yes | No |
startup |
Yes | No |
threaddump |
Yes | No |
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class MyHealthIndicator implements HealthIndicator {
@Override
public Health health() {
int errorCode = check(); // 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();
management:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息
@Component // 只需要将监控检查组件放入容器中即可
public class MyComHealthIndicator extends AbstractHealthIndicator {
/**
* 真实的检查方法
* @param builder
* @throws Exception
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
//mongodb。 获取连接进行测试
Map<String,Object> map = new HashMap<>();
// 检查完成
if(1 == 2){
// builder.up(); //健康
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
// builder.down();
builder.status(Status.OUT_OF_SERVICE);
map.put("err","连接超时");
map.put("ms",3000);
}
// 返回详细的信息
builder.withDetail("code",100)
.withDetails(map);
}
}
常用方式(编写配置文件)
info:
appName: boot-admin
version: 2.0.1
mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值
mavenProjectVersion: @project.version@
常用方式(编写 InfoContributor )
import java.util.Collections;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;
@Component
public class ExampleInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("example",
Collections.singletonMap("key", "value"));
}
}
访问 http://localhost:8080/actuator/info 会输出以上方式返回的所有info信息
@Component
@Endpoint(id = "container")
public class DockerEndpoint {
@ReadOperation
public Map getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}
@WriteOperation
private void restartDocker(){
System.out.println("docker restarted....");
}
}
适用的场景
开发 ReadinessEndpoint 来管理程序是否就绪,或者 Liveness Endpoint 来管理程序是否存活;
当然,这个也可以直接使用 https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-kubernetes-probes
• JVM metrics, report utilization of:
• Various memory and buffer pools
• Statistics related to garbage collection
• Threads utilization
• Number of classes loaded/unloaded
• CPU metrics
• File descriptor metrics
• Kafka consumer and producer metrics
• Log4j2 metrics: record the number of events logged to Log4j2 at each level
• Logback metrics: record the number of events logged to Logback at each level
• Uptime metrics: report a gauge for uptime and a fixed gauge representing the application’s absolute start time
• Tomcat metrics (server.tomcat.mbeanregistry.enabled must be set to true for all Tomcat metrics to be registered)
• Spring Integration metrics
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);
}
引入依赖
<dependency>
<groupId>de.codecentricgroupId>
<artifactId>spring-boot-admin-starter-serverartifactId>
<version>2.3.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
在主启动类上开启监控功能
@EnableAdminServer // 开启服务监控的功能
设置自定义的访问端口号,将该 SpringBoot 服务专门用作监控服务器
server.port=8888
http://localhost:8888 即可访问该监控服务
此时的监控大板上还没有运行的实例信息,需要到另一个 SpringBoot 应用实例中进行注册
导入依赖
<dependency> <groupId>de.codecentricgroupId> <artifactId>spring-boot-admin-starter-clientartifactId> <version>2.3.1version> dependency>
配置连接信息
#设置当前 SpringBoot 客户端需要将应用的信息汇报给那个监控大板服务器的地址,也就是注册当前实例到监控大板服务器 spring.boot.admin.client.url=http://localhost:8888 #使用 ip 进行注册 spring.boot.admin.client.instance.prefer-ip=true #对当前实例命名 spring.application.name=admin-demo
再次访问 http://localhost:8888 即可
profile( 配置文件 )
为了方便多环境适配,springboot 简化了profile 功能。
默认配置文件 application.yaml;任何时候都会加载
指定环境配置文件 application-{env}.yaml
激活指定环境
配置文件激活
命令行激活
默认配置与环境配置同时生效
同名配置项,profile 111配置优先
演示说明
现有 application.properties | application-produce.properties | application-test.properties 如下三个配置文件,其中默认的 application.properties 永远都会加载,其余的配置文件按照实际的生产环境来划分,若在 xxx 环境下使用,则可以命名为 application-xxx.properties,当要使用对应生产环境的配置文件时,就在默认配置文件 application.properties 中进行如下配置
#使用 spring.profiles.active 设置当前环境使用的配置文件 #若对应环境配置文件名为 application-produce.properties,则配置如下 spring.profiles.active=produce
命令行修改
#使用此种方式,能够直接提供命令行修改 spring.profiles.active 的值,以及其他配置文件中属性的值 java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
当满足给定的条件时,指定的配置文件才生效
package com.dhj.profile.bean;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
/*
@Profile("produce") 该注解可以标注在类上,也可以标注在方法上
作用在于,提供参数指定,在指定的某个环境下,配置才生效
也可以写为 @Profile({"produce","default"}),指定在默认的环境下也使用该 produce 配置
*/
@Profile("produce")
@Data
@ConfigurationProperties(prefix = "person")
@Component
public class Boss implements Person {
private String name;
private Integer age;
private String gender;
}
也可以使用在配置类中
@Configuration
public class MyConfig {
// 只在测试环境下生效
@Profile("test")
@Bean
public Dog getDog() {
return new Dog();
}
}
将不同的配置文件进行分组,每组可以有多个不同的配置环境,通过 spring.profiles.active 来指定一组环境进行配置生效
#配置分组1
spring.profiles.group.myprod[0]=yun
spring.profiles.group.myprod[1]=produce
#配置分组2
spring.profiles.group.mytest[0]=test
#使用 spring.profiles.active 设置当前环境使用的配置组生效
spring.profiles.active=myprod
#注:每组的配置分组,索引必须从 0 开始
官方文档
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config
可配置的项
Default properties (specified by setting
SpringApplication.setDefaultProperties
).
@PropertySource
annotations on your@Configuration
classes. Please note that such property sources are not added to theEnvironment
until the application context is being refreshed. This is too late to configure certain properties such aslogging.*
andspring.main.*
which are read before refresh begins.Config data (such as
application.properties
files)A
RandomValuePropertySource
that has properties only inrandom.*
.OS environment variables.
Java System properties (
System.getProperties()
).JNDI attributes from
java:comp/env
.
ServletContext
init parameters.
ServletConfig
init parameters.Properties from
SPRING_APPLICATION_JSON
(inline JSON embedded in an environment variable or system property).Command line arguments.
properties
attribute on your tests. Available on@SpringBootTest
and the test annotations for testing a particular slice of your application.
@TestPropertySource
annotations on your tests.Devtools global settings properties in the
$HOME/.config/spring-boot
directory when devtools is active.
常用:Java属性文件、YAML文件、环境变量、命令行参数;
(1) classpath 根路径
(2) classpath 根路径下 config 目录
(3) jar 包当前目录
(4) jar 包当前目录的 config 目录
(5) /config 子目录的直接子目录根据查找位置的优先顺序,后面的会覆盖前面的配置
当前 jar 包内部的 application.properties 和 application.yml
当前 jar 包内部的 application-{profile}.properties 和 application-{profile}.yml
引用的外部 jar 包的 application.properties 和 application.yml
引用的外部 jar 包的 application-{profile}.properties 和 application-{profile}.yml
根据加载的顺序,后面加载的会覆盖前面的配置
指定环境优先,外部优先,后面的可以覆盖前面的同名配置项
自定义的 starter 作用在于,将开发中常用的功能抽取出来,作为一个 starter ,使用时,直接引入即可
autoconfigure 包中的配置使用 META-INF/spring.factories 中 EnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类
编写自动配置类 xxxAutoConfiguration -> xxxxProperties
引入starter — xxxAutoConfiguration — 容器中放入组件 ---- 绑定xxxProperties ---- 配置项
环境准备
1.创建一个 IDEA 的空项目
2.使用 IDEA 创建一个 Maven 的空项目,用户规定自定义的 starter 需要引入那些依赖,项目名为 dhj-hello-springboot-starter
3.使用 IDEA 创建一个 Spring Initializr 引导项目,用以实现自动配置的功能,项目名为 dhj-hello-springboot-starter-autoconfiguration
- 创建项目后,可能会出现某一个项目的 pom.xml 文件是红橙色,此时,右键该 pom.xml 选择 Add as Maven Project 即可
4.删掉自动配置模块 dhj-hello-springboot-starter-autoconfiguration 中不需要的东西,包括 pom 文件中的单元测试模块,build 中的插件模块,启动类,test 目录,properties 配置文件等
自动配置的代码编写
首先,确定业务逻辑的需求,因为业务逻辑层有一个 sayHello() 方法需要使用到配置文件中的某个属性,此时就可以定义一个【配置文件类】将需要用到的配置封装到 Java 对象中,通过 @ConfigurationProperties 绑定配置文件中的属性,注意,不要直接将【配置文件类】放入容器中,后面在【自动配置类】中加载配置文件并放入容器,通过以上封装,当要使用到配置文件中的某个属性值时,就可以通过对应的【配置文件类】的实例来获取
package com.dhj.hello.proper; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "hello") public class HelloProperties { private String prefixs; private String suffixs; public String getPrefixs() { return prefixs; } public void setPrefixs(String prefixs) { this.prefixs = prefixs; } public String getSuffixs() { return suffixs; } public void setSuffixs(String suffixs) { this.suffixs = suffixs; } }
这时,配置文件中需要的值已经可以通过 HelloProperties 获取到,因此可以继续编写业务逻辑的 HelloService 类,在 HelloService 中通过 HelloProperties 获取业务逻辑需要的配置文件中的属性值
package com.dhj.hello.service; import com.dhj.hello.proper.HelloProperties; import org.springframework.beans.factory.annotation.Autowired; /* service 类在这里是作为创建该 starter 模块的目的所在,默认不要放在容器中,通过配置类来控制 因为该模块最终是作为 stater 来选择性使用的,默认不放入容器中,灵活性更高 */ public class HelloService { /* 若要使用到 HelloService,说明已经经过【自动配置类】进行了 HelloService 的相关配置, 其涉及到的【配置文件类】也必定经过绑定,所以直接从容器中获取到配置文件类 HelloProperties 的实例来调用即可 */ @Autowired HelloProperties helloProperties; public String sayHello(String name) { return helloProperties.getPrefixs() + ":" + name + ">" + helloProperties.getSuffixs(); } }
由于需要控制 HelloService 在容器中的存在情况,因此,需要编写其【自动配置类】来动态配置 HelloService 并将涉及和用到到的【配置文件类放入容器】
package com.dhj.hello.conf; import com.dhj.hello.proper.HelloProperties; import com.dhj.hello.service.HelloService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /* 添加 HelloService 的自动配置类 */ @Configuration // 开启 HelloProperties 与配置文件绑定,并放入容器中,后续便可以直接从容器中拿 @EnableConfigurationProperties(HelloProperties.class) public class HelloServiceAutoConfiguration { // 当容器中没有那个类时,才向容器中放入 HelloService 实例 @ConditionalOnMissingBean(HelloService.class) @Bean //添加 HelloService 到容器 public HelloService helloService(){ return new HelloService(); } }
接下来,进行打包
首先打包自动配置包,注意,再打自动配置类的包之前,需要在 resource 目录下新建 META-INF 目录。在该目录下,新建 spring.factories 文件,在其中指定,项目启动时,自动加载那个包下面的那个类,文件内容示例如下
#指定项目启动时,需要加载那个包中的那个类 # org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 固定写法,代表启用自动加载 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.dhj.hello.conf.HelloServiceAutoConfiguration
使用 Maven 的 clean 和 install 进行打包并安装到本地 Maven 仓库,这样,在 SpringBoot 的 stater 中,就会扫描到 Maven 仓库中配置了 spring.factories 文件的 jar 包
再将自定义 stater 包进行打包,starter 只需要打包,不用进行其他操作
新建一个项目测试
在项目依赖中引入刚刚打包的自定义 stater 的坐标
在项目中,便可以直接通过 @Autowired 获取自定义 stater 中定义的 service 业务逻辑类实例,调用相关功能
整个流程解析
通过自定义的 stater 依赖,找到自定义 stater 绑定的自动配置依赖,通过该依赖找到相关的 jar 包,执行其中的自动配置逻辑,例如将自定义的 service 业务逻辑类实例注入容器,将【配置文件类】注入容器,实现业务逻辑等,最终实现了【在引用了自定义 stater 的工程中,使用自定义 stater 中的相关功能】的效果
创建 SpringApplication
List
去 spring.factories 文件中找org.springframework.boot.Bootstrapper运行 SpringApplication
自定义几个监听组件如下
package com.dhj.helloboot.listener;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
/*
自定义的应用程序上下文初始化器
*/
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("MyApplicationContextInitializer.....");
}
}
package com.dhj.helloboot.listener;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
/*
自定义的应用程序监听器
*/
public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("MyApplicationListener.....");
}
}
package com.dhj.helloboot.listener;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
/*
自定义的应用运行器
*/
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("MyApplicationRunner.....");
}
}
package com.dhj.helloboot.listener;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/*
自定义的命令行运行器
如果 SpringBoot 应用启动需要做一个一次性事情,可以使用该 CommandLineRunner 来完成
*/
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("MyCommandLineRunner.....");
}
}
package com.dhj.helloboot.listener;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
/*
自定义的 Spring 应用程序运行监听器
*/
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
private SpringApplication application;
public MySpringApplicationRunListener(SpringApplication application,String[] args) {
this.application = application;
}
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MySpringApplicationRunListener.....starting.....");
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("MySpringApplicationRunListener.....environmentPrepared.....");
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener.....contextPrepared.....");
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener.....contextLoaded.....");
}
@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener.....started.....");
}
@Override
public void running(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener.....running.....");
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("MySpringApplicationRunListener.....failed.....");
}
}
在 resource 下新建 META-INF 目录,创建 spring.factories 文件,用于加载实例化 spring.factories 中自定义了的组件,j将 MyApplicationContextInitializer | MyApplicationListener | MySpringApplicationRunListener 添加到配置中,具体内容如下
# 自定义的应用程序上下文初始化的 spring.factories 配置
org.springframework.context.ApplicationContextInitializer=\
com.dhj.helloboot.listener.MyApplicationContextInitializer
# 自定义的应用监听器的 spring.factories 配置
org.springframework.context.ApplicationListener=\
com.dhj.helloboot.listener.MyApplicationListener
# 自定义的 Spring 应用程序运行监听器的 spring.factories 配置
org.springframework.boot.SpringApplicationRunListener=\
com.dhj.helloboot.listener.MySpringApplicationRunListener
再将 MyApplicationRunner | MyCommandLineRunner 通过 @Component 注解添加到容器
最后启动 SpringBoot 应用程序,便可看到自定义插件加载执行的顺序
至此,SpringBoot 基础与核心篇的学习告一段落,后续应该注重实践以及源码的分析与理解