一,springboot入门
1.springboot简介
简化spring应用开发
整个spring技术栈的一个大整合
j2ee开发的一站式解决方案
2.微服务
微服务:架构风格
一个应用应该是一组小型服务;可以通过HTTP的方式进行互通;
单体应用:ALL IN ONE
微服务:每一个功能元素最终都是一个可独立替换和独立升级的软件单元;
3.创建一个springboot项目
向服务器发送 /hello ,服务器返回Hello Spring Boot !
1.创建一个普通的maven jar工程
2.导入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.编写springboot的主程序,用来启动项目
/**
* @author yinhuidong
* @createTime 2020-05-04-20:45
*/
//标注一个主程序,说明这是一个SpringBoot应用
@SpringBootApplication
public class SpringBootHelloWorld {
public static void main(String[] args) {
SpringApplication.run(SpringBootHelloWorld.class,args);
}
}
4.编写控制器
/**
* @author yinhuidong
* @createTime 2020-05-04-20:47
*/
//@Controller
//如果整个控制器都需要@ResponseBody注解,那么可以用--替代
@RestController
public class HelloController {
@RequestMapping("/hello")
//@ResponseBody
public String Hello(){
return "hello SpringBoot!";
}
}
5.结果
4.入门程序分析(自动配置原理)
1.pom文件
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.2.6.RELEASE</version>
</parent>
点进去源码看他的上一层:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
由他来真正管理spring boot里面的所有的依赖版本。
2.启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
spring-boot-starter:spring-boot场景启动器;帮我们导入了web模块正常运行所依赖的组件;
Spring Boot将所有的功能场景都抽取出来,做成一个个的starters(启动器),只需要在项目里面引入这些starter相关场景的所有依赖都会导入进来。要用什么功能就导入什么场景的启动器
3.主启动类
//标识在一个主程序上,表示这是一个springboot应用程序的启动器,
// 也是整个应用程序的入口
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
点进这个注解
//表示这是一个springboot的配置类
@SpringBootConfiguration
//开启自动配置,springboot自动配置的核心
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
首先点击去 @SpringBootConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
//spring的原生注解,标识在一个类上,表示这是一个spring的配置类
@Configuration
public @interface SpringBootConfiguration {
回到上一层,点击进去@EnableAutoConfiguration
以前我们需要配置的东西,Spring Boot帮我们自动配置;@EnableAutoConfiguration告诉SpringBoot开启自动配置功能;这样自动配置才能生效;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//自动配置包
@AutoConfigurationPackage
//@import spring的原生注解,往容器中导入AutoConfigurationImportSelector.class
//将主配置类(@SpringBootApplication标注的类)的所在包及下面所有子包里面的所有组件扫描到Spring容器;
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
点击进入 @AutoConfigurationPackage
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//往容器中导入自动配置包的注册器类
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
这两个@Import注解,会将SpringBoot依赖中的
这两个spring.factory文件导入到容器中。
点开spring.factory:
这里面有springboot的大量的自动配置类,容器启动时,就会自动的将我们需要的自动配置导入进来。
J2EE的整体整合解决方案和自动配置都在spring-boot-autoconfigure-2.2.6.RELEASE.jar;
4.@EnableAutoConfiguration
这个注解的底层有一个@Import(AutoConfigurationImportSelector.class),
他往容器中导入了一个AutoConfigurationImportSelector.class。
这个类是什么呢?
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
//这个类里面调用了一个loadMetadata()方法,在容器启动的时候,往容器中加载元数据
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
再看loadMetadata():
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
return loadMetadata(classLoader, PATH);
}
这个PATH是什么?
protected static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";
他往容器中加载的就是这个配置文件,点开可以看到里面都是springboot的自动配置类。
另外一个@Import(AutoConfigurationPackages.Registrar.class),他往容器中导入了一个AutoConfigurationPackages.Registrar.class,这个类是干什么的呢?
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImport(metadata).getPackageName());
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImport(metadata));
}
}
这个类实现了ImportBeanDefinitionRegistrar,这个类在ioc容器创建的源码中出现过,再次不再细说,它会将刚才导入进来的bean在容器创建的时候,全部注册到容器中。
整理一下思路:
@SpringBootApplication
@SpringBootConfiguration
@Configuration
@EnableAutoConfiguration
@AutoConfigurationPackage
@Import(AutoConfigurationPackages.Registrar.class)
@Import(AutoConfigurationImportSelector.class)
@ComponentScan
5.具体的自动配置类
这里以字符编码过滤器为例
//表示这是一个配置类,以前编写的配置文件一样,也可以给容器中添加组件
@Configuration(proxyBeanMethods = false)
//启动指定类的ConfigurationProperties功能;将配置文件中对应的值和HttpEncodingProperties绑定起来;并把HttpEncodingProperties加入到ioc容器中
@EnableConfigurationProperties(HttpProperties.class)
//Spring底层@Conditional注解(Spring注解版),根据不同的条件,如果
满足指定的条件,整个配置类里面的配置就会生效; 判断当前应用是否是web应用,如果是,当前配置类生效
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
//判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;
@ConditionalOnClass(CharacterEncodingFilter.class)
//判断配置文件中是否存在某个配置 spring.http.encoding.enabled;如果不存在,判断也是成立的
//即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的;
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
//他已经和SpringBoot的配置文件映射了
private final HttpProperties.Encoding properties;
//只有一个有参构造器的情况下,参数的值就会从容器中拿
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.getEncoding();
}
@Bean //给容器中添加一个组件,这个组件的某些值需要从properties中获取
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
return filter;
}
5.使用Spring Initializer快速创建Spring Boot项目
IDE都支持使用Spring的项目创建向导快速创建一个Spring Boot项目;
选择我们需要的模板:向导会创建Spring Boot项目;
默认生成的Spring Boot项目;
主程序已经生成好了,我们只需要我们自己的逻辑
resources文件夹中的目录结构:
static:保存所有的静态资源 js css images
templates: 保存所有的模板页面;(Spring Boot默认jar包使用嵌入式的Tomcat,默认不支持JSP页面);可以使用模板引擎(freemarker、thymeleaf);
application.properties:Spring Boot应用的配置文件;可以修改一些默认设置;
二,配置文件
1.配置文件
SpringBoot使用一个全局的配置文件,配置文件名是固定的;
application.properties
application.yml
配置文件的作用:修改SpringBoot自动配置的默认值;SpringBoot在底层都给我们自动配置好;
yaml
标记语言:
以前的配置文件;大多都使用的是 **xxxx.xml**文件;
YAML:**以数据为中心**,比json、xml等更适合做配置文件;
示例
server:
port: 8081
2.yml语法
1)基本语法
k:(空格)v:表示一对键值对(空格必须有);
以空格的缩进来控制层级关系;只要是左对齐的一列数据,都是同一个层级的
server:
port: 8081
path: /hello
属性和值也是大小写敏感
2)值的写法
字面量:普通的值(数字,字符串,布尔)
k: v:字面直接来写;
字符串默认不用加上单引号或者双引号;
“”:双引号;不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思
name: “zhangsan \n lisi”:输出;zhangsan 换行 lisi
‘’:单引号;会转义特殊字符,特殊字符最终只是一个普通的字符串数据
name: ‘zhangsan \n lisi’:输出;zhangsan \n lisi
对象、Map(属性和值)(键值对):
k: v:在下一行来写对象的属性和值的关系;注意缩进
对象还是k: v的方式
friends:
lastName: zhangsan
age: 20
行内写法:
friends: {lastName: zhangsan,age: 18}
数组(List、Set):
用- 值表示数组中的一个元素
pets:
- cat
- dog
- pig
行内写法:
pets: [cat,dog,pig]
3)配置文件值注入
Person类
/**
* @author yinhuidong
* @createTime 2020-05-04-23:14
* 将配置文件中配置的每一个属性的值,映射到这个组件中
* @ConfigurationProperties:告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定;
* prefix = "person":配置文件中哪个下面的所有属性进行一一映射
*
* 只有这个组件是容器中的组件,才能容器提供的@ConfigurationProperties功能;
*/
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
private String lastName;
private String email;
private Map<String,Object> maps;
private List<String> lists;
private Dog dog;
application.yaml
person:
last-name: 张三
email: 1972039773@qq.com
maps:
k1: v1
k2: v2
lists:
- k1
- k2
- k3
dog:
name: 毛毛
age: 9
测试类
/**
* @author yinhuidong
* @createTime 2020-05-04-23:35
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class Test1 {
@Autowired
private Person person;
@Test
public void test1(){
System.out.println(person);
}
}
我们可以导入配置文件处理器,以后编写配置就有提示了
<!--导入配置文件处理器,配置文件进行绑定就会有提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
4)@Value获取值和@ConfigurationProperties获取值比较
@ConfigurationProperties | @Value | |
---|---|---|
功能 | 批量注入配置文件中的属性 | 一个个指定 |
松散绑定(松散语法) | 支持 | 不支持 |
SpEL | 不支持 | 支持 |
JSR303数据校验 | 支持 | 不支持 |
复杂类型封装 | 支持 | 不支持 |
配置文件yml还是properties他们都能获取到值;
如果说,我们只是在某个业务逻辑中需要获取一下配置文件中的某项值,使用@Value;
如果说,我们专门编写了一个javaBean来和配置文件进行映射,我们就直接使用@ConfigurationProperties;
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
/**
* <bean class="Person">
* <property name="lastName" value="字面量/${key}从环境变量、配置文件中获取值/#{SpEL}"></property>
* <bean/>
*/
@Value("21")
private String lastName;
@Value("#{person.email}")
private String email;
private Map<String,Object> maps;
private List<String> lists;
private Dog dog;
5)配置文件注入值数据校验
@Component
@ConfigurationProperties(prefix = "person")
@Validated//校验
public class Person {
/**
* <bean class="Person">
* <property name="lastName" value="字面量/${key}从环境变量、配置文件中获取值/#{SpEL}"></property>
* <bean/>
*/
@Value("21")
private String lastName;
@Email
private String email;
private Map<String,Object> maps;
private List<String> lists;
private Dog dog;
3.@PropertySource&@ImportResource&@Bean
1)@PropertySource:加载指定的配置文件;
@Component
@ConfigurationProperties(prefix = "person")
//@Validated//校验
@PropertySource("classpath:person.properties")
public class Person {
/**
* <bean class="Person">
* <property name="lastName" value="字面量/${key}从环境变量、配置文件中获取值/#{SpEL}"></property>
* <bean/>
*/
@Value("21")
private String lastName;
@Email
private String email;
private Map<String,Object> maps;
private List<String> lists;
private Dog dog;
2)@ImportResource:导入Spring的配置文件,让配置文件里面的内容生效;
Spring Boot里面没有Spring的配置文件,我们自己编写的配置文件,也不能自动识别;
想让Spring的配置文件生效,加载进来;@ImportResource标注在一个配置类上
主配置类
@SpringBootApplication
@ImportResource("classpath:applicationContext.xml")
public class Springboot01Application {
public static void main(String[] args) {
SpringApplication.run(Springboot01Application.class, args);
}
}
spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="personService" class="com.yhd.springboot01.service.PersonService"></bean>
</beans>
测试类
@Autowired
private PersonService service;
@Test
public void test(){
System.out.println(service);
}
3)SpringBoot推荐给容器中添加组件的方式;推荐使用全注解的方式
1、配置类@Configuration------>Spring配置文件
2、使用 @Bean 给容器中添加组件
/**
* @author yinhuidong
* @createTime 2020-05-05-0:39
*
* @Configuration:指明当前类是一个配置类;就是来替代之前的Spring配置文件
*
* 在配置文件中用<bean><bean/>标签添加组件
*
*/
@Configuration
public class MyConfig {
@Bean
public PersonService getService(){
return new PersonService();
}
}
4.配置文件占位符
1.随机数
${random.value}、${random.int}、${random.long}
${random.int(10)}、${random.int[1024,65536]}
2.占位符获取之前配置的值,如果没有可以用:指定默认值
person.last‐name=张三${random.uuid}
person.age=${random.int}
person.birth=2017/12/15
person.boss=false
person.maps.k1=v1
person.maps.k2=14
person.lists=a,b,c
person.dog.name=${person.hello:hello}_dog
person.dog.age=15
5.Profile
1)多Profile文件
我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml
默认使用application.properties的配置;
2)yml支持多文档块方式
server:
port: 8081
spring:
profiles:
active: prod
‐‐‐
server:
port: 8083
spring:
profiles: dev
‐‐‐
server:
port: 8084
spring:
profiles: prod #指定属于哪个环境
3)激活指定的profile
1.在配置文件中指定spring.profile.active=dev
2.命令行:
java -jar spring-boot-02-config-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev;
可以直接在测试的时候,配置传入命令行参数
3.虚拟机参数:
-Dspring.profiles.active=dev
6.配置文件加载位置
springboot启动会扫描以下位置的application.properties或者application.yaml文件作为spring boot的默认配置文件。
–file:./config/
–file:./
–classpath:/config/
–classpath:/
优先级由高到低,高优先级的配置会覆盖低优先级的配置。
spring boot 会从这四个位置全部加载主配置文件,互补配置。
我们还可以通过spring.config.location来改变默认的配置文件位置
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;指定配置文件和默认加载的这些配置文件共同起作用形成互补配置;
java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --spring.config.location=G:/application.properties
7.外部配置加载顺序
SpringBoot也可以从以下位置加载配置; 优先级从高到低;高优先级的配置覆盖低优先级的配置,所有的配置会形成互补配置
1)命令行参数
所有的配置都可以在命令行上进行指定
java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --server.port=8087 --server.context-path=/abc
多个配置用空格分开; --配置项=值
2)来自java:comp/env的JNDI属性
3)Java系统属性(System.getProperties()
4)操作系统环境变量
5)RandomValuePropertySource配置的random.*属性值
由jar包外向jar包内进行寻找;
优先加载带profile
6)jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件
7)jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件
再来加载不带profile
8)jar包外部的application.properties或application.yml(不带spring.profile)配置文件
9)jar包内部的application.properties或application.yml(不带spring.profile)配置文件
10)@Configuration注解类上的@PropertySource
11)通过SpringApplication.setDefaultProperties指定的默认属性
8.自动配置原理
配置文件到底能写什么?怎么写?自动配置原理;
配置文件能配置的属性参照
1.自动配置原理
Spring boot 启动的时候加载主配置类,开启了自动配置功能 @EnableAutoConfiguration
@EnableAutoConfiguration 作用:
利用EnableAutoConfigurationImportSelector给容器中导入一些组件
可以查看selectImports()方法的内容
List configurations = getCandidateConfigurations(annotationMetadata, attributes);获取候选的配置
SpringFactoriesLoader.loadFactoryNames()
扫描所有jar包类路径下 META‐INF/spring.factories
把扫描到的这些文件的内容包装成properties对象
从properties中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在容器中
所有在配置文件中能配置的属性都是在xxxxProperties类中封装着‘;配置文件能配置什么就可以参照某个功能对应的这个属性类
@ConfigurationProperties(prefix = "spring.http")//从配置文件中获取指定的值和bean的属
性进行绑定
public class HttpProperties {
public static class Encoding {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private Charset charset = DEFAULT_CHARSET;
private Boolean force;
private Boolean forceRequest;
private Boolean forceResponse;
private Map<Locale, Charset> mapping;
总结:
springBoot启动会加载大量的自动配置类
我们看我们需要的功能有没有SpringBoot默认写好的自动配置类
我们再来看这个自动配置类中到底配置了哪些组件
给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们就可以在配置文件中指定这
些属性的值;
2.细节
1、@Conditional派生注解(Spring注解版原生的@Conditional作用)
作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置类里面的所有内容才生效;
@Conditional扩展注解 | 作用(判断是否满足当前指定条件) |
---|---|
@ConditionalOnJava | 系统的java版本是否符合要求 |
@ConditionalOnBean | 容器中存在指定Bean; |
@ConditionalOnMissingBean | 容器中不存在指定Bean; |
@ConditionalOnExpression | 满足SpEL表达式指定 |
@ConditionalOnClass | 系统中有指定的类 |
@ConditionalOnMissingClass | 系统中没有指定的类 |
@ConditionalOnSingleCandidate | 容器中只有一个指定的Bean,或者这个Bean是首选Bean |
@ConditionalOnProperty | 系统中指定的属性是否有指定的值 |
@ConditionalOnResource | 类路径下是否存在指定资源文件 |
@ConditionalOnWebApplication | 当前是web环境 |
@ConditionalOnNotWebApplication | 当前不是web环境 |
@ConditionalOnJndi | JNDI存在指定项 |
自动配置类必须在一定的条件下才能生效;
我们怎么知道哪些自动配置类生效;
我们可以通过启用 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置
类生效;
Positive matches:
-----------------
AopAutoConfiguration matched:
- @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition)
AopAutoConfiguration.ClassProxyingConfiguration matched:
- @ConditionalOnMissingClass did not find unwanted class 'org.aspectj.weaver.Advice' (OnClassCondition)
- @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition)
DispatcherServletAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition)
- found 'session' scope (OnWebApplicationCondition)
三,日志
1.日志框架
开发一个日志框架
1、System.out.println("");将关键数据打印在控制台;去掉?写在一个文件?
2、框架来记录系统的一些运行时信息;日志框架 ; zhanglogging.jar;
3、高大上的几个功能?异步模式?自动归档?xxxx? zhanglogging-good.jar?
4、将以前框架卸下来?换上新的框架,重新修改之前相关的API;zhanglogging-prefect.jar;
5、JDBC---数据库驱动;
写了一个统一的接口层;日志门面(日志的一个抽象层);logging-abstract.jar;
给项目中导入具体的日志实现就行了;我们之前的日志框架都是实现的抽象层;
市面上的日志框架
JUL、JCL、Jboss-logging、logback、log4j、log4j2、slf4j…
日志门面 (日志的抽象层) | 日志实现 |
---|---|
JCL(Jakarta Commons Logging) SLF4j(Simple Logging Facade for Java) jboss-logging | Log4j JUL(java.util.logging)Log4j2 Logback |
左边选一个抽象层接口,右边选一个接口的实现类。
接口:SLF4J
日志实现:Logback
SpringBoot底层是Spring框架,Spring框架默认是JCL
SpringBoot选用SLF4j和logback
2.SLF4J使用
1.如何在开发中使用SLF4j
以后开发的时候,日志记录方法的调用,不应该来直接调用日志的实现类,而是调用日志抽象层里面的方法;给系统里面导入slf4j的jar和logback的实现jar。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}
图示:
每一个日志的实现框架都有自己的配置文件。使用slf4j以后,配置文件还是做成日志实现框架本身的配置文件。
2.遗留问题
a(slf4j+logback): Spring(commons-logging)、Hibernate(jboss-logging)、MyBatis、xxxx
统一日志记录,即使是别的框架和我一起统一使用slf4j进行输出?
如何让系统中所有的日志都统一到slf4j?
1、将系统中其他日志框架先排除出去;
2、用中间包来替换原有的日志框架;
3、我们导入slf4j其他的实现
3,SpringBoot日志关系
SpringBoot日志启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
底层依赖关系
总结:
1)、SpringBoot底层也是使用slf4j+logback的方式进行日志记录
2)、SpringBoot也把其他的日志都替换成了slf4j;
3)、中间替换包?
@SuppressWarnings("rawtypes")
public abstract class LogFactory {
static String UNSUPPORTED_OPERATION_IN_JCL_OVER_SLF4J =
"http://www.slf4j.org/codes.html#unsupported_operation_in_jcl_over_slf4j";
static LogFactory logFactory = new SLF4JLogFactory();
如果我们要引入其他框架?一定要把这个框架的默认日志依赖移除掉
Spring框架用的是commons-logging;
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring‐core</artifactId>
<exclusions>
<exclusion>
<groupId>commons‐logging</groupId>
<artifactId>commons‐logging</artifactId>
</exclusion>
</exclusions>
</dependency>
SpringBoot能自动适配所有的日志,而且底层使用slf4j+logback的方式记录日志,引入其他框架的时候,只需要把这个框架依赖的日志框架排除掉即可;
4.日志使用
1.默认配置
SpringBoot默认帮我们配置好了日志
//记录器
Logger logger = LoggerFactory.getLogger(getClass());
@Test
public void contextLoads() {
//System.out.println();
//日志的级别;
//由低到高 trace<debug<info<warn<error
//可以调整输出的日志级别;日志就只会在这个级别以以后的高级别生效
logger.trace("这是trace日志...");
logger.debug("这是debug日志...");
//SpringBoot默认给我们使用的是info级别的,没有指定级别的就用SpringBoot默认规定的级别;root
级别
logger.info("这是info日志...");
logger.warn("这是warn日志...");
logger.error("这是error日志...");
}
SpringBoot修改日志的默认配置
logging.level.com.atguigu=trace
#logging.path=
# 不指定路径在当前项目下生成springboot.log日志
# 可以指定完整的路径;
#logging.file=G:/springboot.log
# 在当前磁盘的根路径下创建spring文件夹和里面的log文件夹;使用 spring.log 作为默认文件
logging.path=/spring/log
# 在控制台输出的日志的格式
logging.pattern.console=%d{yyyy‐MM‐dd} [%thread] %‐5level %logger{50} ‐ %msg%n
# 指定文件中日志输出的格式
logging.pattern.file=%d{yyyy‐MM‐dd} === [%thread] === %‐5level === %logger{50} ==== %msg%n
日志输出格式
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%‐5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
‐‐>
%d{yyyy‐MM‐dd HH:mm:ss.SSS} [%thread] %‐5level %logger{50} ‐ %msg%n
logging.file | logging.path | Example | Description |
---|---|---|---|
(none) | (none) | 只在控制台输出 | |
指定文件名 | (none) | my.log | 输出日志到my.log文件 |
(none) | 指定目录 | /var/log | 输出到指定目录的 spring.log 文件中 |
2.指定配置
给类路径下放上每个日志框架自己的配置文件即可;SpringBoot就不使用他默认配置的了
Logging System | Customization |
---|---|
Logback | logback-spring.xml , logback-spring.groovy , logback.xml or logback.groovy |
Log4j2 | log4j2-spring.xml or log4j2.xml |
JDK (Java Util Logging) | logging.properties |
logback.xml:直接就被日志框架识别了;
logback-spring.xml:日志框架就不直接加载日志的配置项,由SpringBoot解析日志配置,可以使用SpringBoot
的高级Profile功能
<springProfile name="staging">
<!‐‐ configuration to be enabled when the "staging" profile is active ‐‐>
可以指定某段配置只在某个环境下生效
</springProfile>
example:
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<!‐‐
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%‐5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
‐‐>
<layout class="ch.qos.logback.classic.PatternLayout">
<springProfile name="dev">
<pattern>%d{yyyy‐MM‐dd HH:mm:ss.SSS} ‐‐‐‐> [%thread] ‐‐‐> %‐5level
%logger{50} ‐ %msg%n</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%d{yyyy‐MM‐dd HH:mm:ss.SSS} ==== [%thread] ==== %‐5level
%logger{50} ‐ %msg%n</pattern>
</springProfile>
</layout>
</appender>
5.切换日志框架
sl4j+log4j
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
<exclusion>
<artifactId>log4j-over-slf4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
切换为log4j2
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration scan="false" scanPeriod="60 seconds" debug="false">
<!-- 定义日志的根目录 -->
<property name="LOG_HOME" value="/app/log" />
<!-- 定义日志文件名称 -->
<property name="appName" value="atguigu-springboot"></property>
<!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<!--
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%-5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
-->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} 尹会东 [%thread] %-5level %logger{50} - %msg%n</pattern>
</layout>
</appender>
<!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 指定日志文件的名称 -->
<file>${LOG_HOME}/${appName}.log</file>
<!--
当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--
滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动
%i:当文件大小超过maxFileSize时,按照i进行文件滚动
-->
<fileNamePattern>${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!--
可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
那些为了归档而创建的目录也会被删除。
-->
<MaxHistory>365</MaxHistory>
<!--
当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 日志输出格式: -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
</layout>
</appender>
<!--
logger主要用于存放日志对象,也可以定义日志类型、级别
name:表示匹配的logger类型前缀,也就是包的前半部分
level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERROR
additivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,
false:表示只用当前logger的appender-ref,true:
表示当前logger的appender-ref和rootLogger的appender-ref都有效
-->
<!-- hibernate logger -->
<logger name="com.atguigu" level="debug" />
<!-- Spring framework logger -->
<logger name="org.springframework" level="debug" additivity="false"></logger>
<!--
root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。
-->
<root level="info">
<appender-ref ref="stdout" />
<appender-ref ref="appLogAppender" />
</root>
</configuration>
四,WEB开发
1.简介
使用SpringBoot
1)、创建SpringBoot应用,选中我们需要的模块;
2)、SpringBoot已经默认将这些场景配置好了,只需要在配置文件中指定少量配置就可以运行起来
3)、自己编写业务代码;
自动配置原理?
这个场景SpringBoot帮我们配置了什么?能不能修改?能修改哪些配置?能不能扩展?xxx
xxxxAutoConfiguration:帮我们给容器中自动配置组件;
xxxxProperties:配置类来封装配置文件的内容;
2、SpringBoot对静态资源的映射规则
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
//可以设置和静态资源有关的参数,缓存时间等
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
WebMvcAuotConfiguration:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Integer cachePeriod = this.resourceProperties.getCachePeriod();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(
registry.addResourceHandler("/webjars/**")
.addResourceLocations(
"classpath:/META‐INF/resources/webjars/")
.setCachePeriod(cachePeriod));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
//静态资源文件夹映射
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(
registry.addResourceHandler(staticPathPattern)
.addResourceLocations(
this.resourceProperties.getStaticLocations())
.setCachePeriod(cachePeriod));
}
}
//配置欢迎页映射
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(
ResourceProperties resourceProperties) {
return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
}
//配置喜欢的图标
@Configuration
@ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
public static class FaviconConfiguration {
private final ResourceProperties resourceProperties;
public FaviconConfiguration(ResourceProperties resourceProperties) {
this.resourceProperties = resourceProperties;
}
@Bean
public SimpleUrlHandlerMapping faviconHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
//所有 **/favicon.ico
mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
faviconRequestHandler()));
return mapping;
}
@Bean
public ResourceHttpRequestHandler faviconRequestHandler() {
ResourceHttpRequestHandler requestHandler = new
ResourceHttpRequestHandler();
requestHandler
.setLocations(this.resourceProperties.getFaviconLocations());
return requestHandler;
}
}
1)所有 /webjars/** ,都去 classpath:/META-INF/resources/webjars/ 找资源;
webjars:以jar包的方式引入静态资源;
www.webjars.org
<!--jquery-->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.0</version>
</dependency>
<!--bootstrap-->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.7</version>
</dependency>
localhost:8080/webjars/jquery/3.5.0/jquery.js
2)"/**" 访问当前项目的任何资源,都去(静态资源的文件夹)找映射
"classpath:/META‐INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
"/":当前项目的根路径
localhost:8080/abc === 去静态资源文件夹里面找abc
3)欢迎页; 静态资源文件夹下的所有index.html页面;被"/**"映射;
localhost:8080/ 找index页面
4)所有的 **/favicon.ico 都是在静态资源文件下找;
3.模板引擎
SpringBoot推荐的Thymeleaf;
语法更简单,功能更强大;
1)引入thymeleaf
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
切换thymeleaf版本
<properties>
<java.version>1.8</java.version>
<springboot-thymeleaf.version>3.0.9.RELEASE</springboot-thymeleaf.version>
<thymeleaf-layout-dialect.version>2.0.4</thymeleaf-layout-dialect.version>
</properties>
2)thymeleaf使用
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = Charset.forName("UTF‐8");
private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html");
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
//
只要我们把HTML页面放在classpath:/templates/,thymeleaf就能自动渲染;
使用:
1、导入thymeleaf的名称空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
2、使用thymeleaf语法;
@RequestMapping("/test")
public String test(Map<String,Object> map){
map.put("hello","welcome");
return "index";
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF‐8">
<title>Title</title>
</head>
<body>
<h1>成功!</h1>
<!‐‐th:text 将div里面的文本内容设置为 ‐‐>
<div th:text="${hello}">这是显示欢迎信息</div>
</body>
</html>
3.语法规则
1)th:text ;改变当前元素里面的文本内容
th:任意html属性;来替换原生的属性
2)表达式
Simple expressions:(表达式语法)
Variable Expressions: ${...}:获取变量值;OGNL;
1)、获取对象的属性、调用方法
2)、使用内置的基本对象:
#ctx : the context object.
#vars: the context variables.
#locale : the context locale.
#request : (only in Web Contexts) the HttpServletRequest object.
#response : (only in Web Contexts) the HttpServletResponse object.
#session : (only in Web Contexts) the HttpSession object.
#servletContext : (only in Web Contexts) the ServletContext object.
${session.foo}
3)、内置的一些工具对象:
#execInfo : information about the template being processed.
#messages : methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.
#uris : methods for escaping parts of URLs/URIs
#conversions : methods for executing the configured conversion service (if any).
#dates : methods for java.util.Date objects: formatting, component extraction, etc.
#calendars : analogous to #dates , but for java.util.Calendar objects.
#numbers : methods for formatting numeric objects.
#strings : methods for String objects: contains, startsWith, prepending/appending, etc.
#objects : methods for objects in general.
#bools : methods for boolean evaluation.
#arrays : methods for arrays.
#lists : methods for lists.
#sets : methods for sets.
#maps : methods for maps.
#aggregates : methods for creating aggregates on arrays or collections.
#ids : methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).
Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
补充:配合 th:object="${session.user}:
<div th:object="${session.user}">
<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>
Message Expressions: #{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL;
@{/order/process(execId=${execId},execType='FAST')}
Fragment Expressions: ~{...}:片段引用表达式
<div th:insert="~{commons :: main}">...</div>
Literals(字面量)
Text literals: 'one text' , 'Another one!' ,…
Number literals: 0 , 34 , 3.0 , 12.3 ,…
Boolean literals: true , false
Null literal: null
Literal tokens: one , sometext , main ,…
Text operations:(文本操作)
String concatenation: +
Literal substitutions: |The name is ${name}|
Arithmetic operations:(数学运算)
Binary operators: + , - , * , / , %
Minus sign (unary operator): -
Boolean operations:(布尔运算)
Binary operators: and , or
Boolean negation (unary operator): ! , not
Comparisons and equality:(比较运算)
Comparators: > , < , >= , <= ( gt , lt , ge , le )
Equality operators: == , != ( eq , ne )
Conditional operators:条件运算(三元运算符)
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
Special tokens:
No-Operation: _
4.springmvc自动配置
1.Spring MVC auto-configuration
springboot自动配置好了springmvc
以下是springboot对springmvc的默认配置(WebMvcAutoConfiguration)
1)自动配置了viewResolver(视图解析器)
ContentNegotiatingViewResolver:组合所有的视图解析器的;
如何定制:我们可以自己给容器中添加一个视图解析器;自动的将其组合进来。
2)支持静态资源文件夹路径wbjars
index.html 静态首页访问
faicon.ico 定制的logo
3)自动注册了类型转换器和格式化器
Converter:转换器; public String hello(User user):类型转换使用Converter
Formatter 格式化器; 2017.12.17===Date;
@Bean
@ConditionalOnProperty(prefix = "spring.mvc", name = "date‐format")//在文件中配置日期格式化的规则
public Formatter<Date> dateFormatter() {
return new DateFormatter(this.mvcProperties.getDateFormat());//日期格式化组件
}
自己添加的格式化器转换器,我们只需要放在容器中即可
4)Support for HttpMessageConverters (see below).
支持消息转换
HttpMessageConverter:SpringMVC用来转换Http请求和响应的;User—Json;
HttpMessageConverters 是从容器中确定;获取所有的HttpMessageConverter;
自己给容器中添加HttpMessageConverter,只需要将自己的组件注册容器中
(@Bean,@Component)
5)定义错误代码生成规则
MessageCodesResolver
6)ConfigurableWebBindingInitializer
其主要作用就是 初始化WebDataBinder;将请求的参数转化为对应的JavaBean,并且会结合上面的类型、格式转换一起使用。
protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() {
try {
//从容器中获取
return (ConfigurableWebBindingInitializer)this.beanFactory.getBean(ConfigurableWebBindingInitializer.class);
} catch (NoSuchBeanDefinitionException var2) {
return super.getConfigurableWebBindingInitializer();
}
}
可以发现ConfigurableWebBindingInitializer
是从容器(beanFactory
)中获取到的,所以我们可以配置一个ConfigurableWebBindingInitializer
来替换默认的,只需要在容器中添加一个我们自定义的转换器即可。
2.扩展SpringMVC
<mvc:view‐controller path="/hello" view‐name="success"/>
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/hello"/>
<bean></bean>
</mvc:interceptor>
</mvc:interceptors>
编写一个配置类(@Configuration),是WebMvcConfigure类型;不能标注@EnableWebMvc;
既保留了所有的自动配置,也能用我们扩展的配置;
/**
* @author yhd
* @createtime 2020/10/5 21:15
*/
@SpringBootConfiguration
public class MyWebMvcConfig implements WebMvcConfigurer {
/**
* 根据url跳转到指定的路径
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//浏览器发送/yhd请求,会跳转到 success 页面
registry.addViewController("/yhd").setViewName("success");
}
/**
* 自定义路径匹配规则
* 一般用不上
* @param configurer
*/
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// 是否通过请求参数来决定返回数据,默认为false
configurer.favorParameter(true)
.ignoreAcceptHeader(true) // 不检查Accept请求头
.parameterName("zyMediaType")// 参数名称,就是通过什么样的参数来获取返回值
.defaultContentType(MediaType.APPLICATION_JSON)
.mediaType("json",MediaType.APPLICATION_JSON)
.mediaType("xml",MediaType.APPLICATION_XML);
// mediaType此方法是从请求参数扩展名(也就是最后一个.后面的值),
// 然后绑定在parameterName上面,比如/admin/getUser.xml 等同于/admin/getUser?zyMediaType=xml
// 如果不需要这种后缀的,那么就是全部通过参数的方式传递到后台
}
/**
* 配置异步请求处理选项
* @param configurer
*/
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
}
/**
* 默认静态资源处理器
* @param configurer
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
}
/**
* 格式化转换器
* @param registry
*/
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new Formatter<Date>() {
@Override
public Date parse(String text, Locale locale) throws ParseException {
return null;
}
@Override
public String print(Date object, Locale locale) {
return null;
}
});
}
/**
* 添加拦截器
* addInterceptor:需要一个实现HandlerInterceptor接口的拦截器实例
* addPathPatterns:用于设置拦截器的过滤路径规则;addPathPatterns("/**")对所有请求都拦截
* excludePathPatterns:用于设置不需要拦截的过滤规则
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
}
/**
* 自定义静态资源映射目录
* addResoureHandler:指的是对外暴露的访问路径
* addResourceLocations:指的是内部文件放置的目录
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/my/**").addResourceLocations("classpath:/my/");
}
/**
* 配置跨域
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
}
/**
* 视图解析器
* @param registry
*/
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.viewResolver(resourceViewResolver());
}
/**
* 说明:这个方法是我自己定义的,
* 配置请求视图映射
* @return
*/
@Bean
public InternalResourceViewResolver resourceViewResolver()
{
InternalResourceViewResolver internalResourceViewResolver = new InternalResourceViewResolver();
//请求视图文件的前缀地址
internalResourceViewResolver.setPrefix("/WEB-INF/views/");
//请求视图文件的后缀
internalResourceViewResolver.setSuffix(".jsp");
return internalResourceViewResolver;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
}
/**
* 消息内容转换配置
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
/**
* 配置异常处理器
* @param resolvers
*/
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
@Override
public Validator getValidator() {
return null;
}
@Override
public MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {
原理:
1)WebMvcAutoConfiguration是SpringMVC的自动配置类
2)自动配置类在做其他自动配置时会导入**@Import(EnableWebMvcConfiguration.class)**
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
可以看出EnableWebMvcConfiguration.class是自动配置类的一个内部类。并且继承了DelegatingWebMvcConfiguration
@Configuration(proxyBeanMethods = false)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
进入DelegatingWebMvcConfiguration
//从容器中获取所有的WebMvcConfigurer
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
函数中setConfigurers
中可以看到,其将容器中所有的WebMvcConfigurer
配置都获取到了。
3)容器中所有的WebMvcConfigurer都会一起起作用;
4)我们的配置类也会被调用;
效果:SpringMVC的自动配置和我们的扩展配置都会起作用;
图示:
3.全面接管SpringMVC
SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己配置;所有的SpringMVC的自动配置都失效了
/**
* @author yinhuidong
* @createTime 2020-05-06-11:34
*/
@EnableWebMvc
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/atguigu").setViewName("index");
}
}
原理:为什么@EnableWebMvc自动配置就失效了;
1)@EnableWebMvc的核心
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
2)发现这个注解上导入了DelegatingWebMvcConfiguration.class
点击去查看:
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
3)发现这个类继承了WebMvcConfigurationSupport
4)然后再回到SpringMVC的自动配置类–WebMvcAutoConfiguration
5)导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能;
5.如何修改SpringBoot自动配置
1)、SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(@Bean、@Component)如果有就用用户配置的,如果没有,才自动配置;如果有些组件可以有多个(ViewResolver)将用户配置的和自己默认的组合起来;
2)、在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置
3)、在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置
6.RestfulCRUD
1.默认访问首页
/**
* @author yinhuidong
* @createTime 2020-05-06-11:34
* SpringMVC功能拓展
*/
//@EnableWebMvc//不要全面接管springmvc
@Configuration
public class MyMvcConfig extends WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/atguigu").setViewName("index");
}
/**
* 在SpringMVC的拓展配置文件里设置:
* <h1>默认访问首页</h1>
* @return
*/
@Bean
public WebMvcConfigurerAdapter toIndex(){
WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
}
};
return adapter;
}
}
页面引入静态资源
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Signin Template for Bootstrap</title>
<!--引入wabjars的bootstrap-->
<link href="asserts/css/bootstrap.min.css" th:href="@{/webjars/bootstrap/4.0.0/css/bootstrap.css}" rel="stylesheet">
<!-- 引入自定义在resource/static/的css -->
<link href="asserts/css/signin.css" th:href="@{/asserts/css/signin.css}" rel="stylesheet">
</head>
2.国际化
springmvc处理国际化:
1)、编写国际化配置文件;
2)、使用ResourceBundleMessageSource管理国际化资源文件
3)、在页面使用fmt:message取出国际化内容
springboot处理国际化:
1)编写国际化配置文件
login.password=密码
login.remember=记住我
login.sign=登录
login.tip=请登录
login.username=用户名
2)SpringBoot自动配置好了管理国际化资源文件的组件;
public class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = {};
@Bean
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
protected static class ResourceBundleCondition extends SpringBootCondition {
private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();
//=可以通过spring。message。basename设置国际化文件名称位置,默认为message
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
ConditionOutcome outcome = cache.get(basename);
if (outcome == null) {
outcome = getMatchOutcomeForBasename(context, basename);
cache.put(basename, outcome);
}
return outcome;
}
点击进入MessageSourceProperties
public class MessageSourceProperties {
/**
* Comma-separated list of basenames (essentially a fully-qualified classpath
* location), each following the ResourceBundle convention with relaxed support for
* slash based locations. If it doesn't contain a package qualifier (such as
* "org.mypackage"), it will be resolved from the classpath root.
*/
private String basename = "messages";
//可以通过spring。message。basename设置国际化文件名称位置,默认为message
application.properties
#修改项目访问路径
server.servlet.context-path=/crud
#设置国际化文件位置
spring.messages.basename=i18n.login
3)去页面获取国际化的值
<body class="text-center">
<form class="form-signin" action="dashboard.html">
<img class="mb-4" src="asserts/img/bootstrap-solid.svg" th:src="@{/asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
<label class="sr-only" th:text="#{login.username}">Username</label>
<input type="text" class="form-control" placeholder="Username" th:placeholder="#{login.username}" required="" autofocus="">
<label class="sr-only" th:text="#{login.password}">Password</label>
<input type="password" class="form-control" placeholder="Password" required="" th:placeholder="#{login.password}">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"> [[#{login.remember}]]
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.sign}">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2019-2020</p>
<a class="btn btn-sm">中文</a>
<a class="btn btn-sm">English</a>
</form>
</body>
此时就达到了页面国际化的效果。
能够根据浏览器语言切换国际化的原因:
Springmvc的自动配置类:WebMvcAutoConfiguration
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
//默认的就是根据请求头带来的区域信息获取Locale进行国际化
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
区域信息对象解析器:LocaleResolver
public interface LocaleResolver {
Locale resolveLocale(HttpServletRequest request);
void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);
}
4)需求:页面点击按钮切换中英文。
在请求链接上绑定区域信息
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
自定义区域信息解析器
/**
* @author yinhuidong
* @createTime 2020-05-06-15:08
* 自定义区域信息解析器
*/
public class MyLocalResever implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String l = request.getParameter("l");
Locale locale = Locale.getDefault();
if (!StringUtils.isEmpty(l)) {
String[] s = l.split("_");
locale=new Locale(s[0],s[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
将自定义的区域信息解析器注册到springboot容器中
/**
* 将自定义的区域信息解析器注册到springboot容器中
* @return
*/
//此处方法返回值和方法名都不能改动,因为是要跟源码中的实现方法一致
@Bean
public LocaleResolver localeResolver(){
return new MyLocalResever();
}
3.登录
开发期间模板引擎页面修改以后,要实时生效
# 禁用缓存
spring.thymeleaf.cache=false
页面修改完成以后ctrl+f9:重新编译;
控制层
/**
* @author yinhuidong
* @createTime 2020-05-06-16:53
*/
@Controller
@RequestMapping("/user")
public class UserController {
@PostMapping("/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Map<String,Object> map,
HttpSession session){
if (!StringUtils.isEmpty(username)&&"root".equals(password)){
session.setAttribute("loginUser",username);
//防止表单重复提交
return "redirect:/main.html";
}
map.put("msg","用户名或密码错误!");
return "index";
}
}
自定义拦截器(登录检查)
/**
* @author yinhuidong
* @createTime 2020-05-06-17:26
* 自定义拦截器,登录检查
*/
public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("loginUser");
if (user==null){
//未登陆,返回登陆页面
request.setAttribute("msg","请先登录后访问!");
request.getRequestDispatcher("/index.html").forward(request,response);
return false;
}
return true;
}
}
将拦截器注册进容器
/**
* 在SpringMVC的拓展配置文件里设置:
* <h1>默认访问首页</h1>
* @return
*/
//所有的WebMvcConfigurerAdapter组件都会一起起作用
@Bean //将组件注册在容器
public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){
WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
registry.addViewController("/main.html").setViewName("dashboard");
}
//注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//super.addInterceptors(registry);
//静态资源; *.css , *.js
//SpringBoot已经做好了静态资源映射,但是如果自定义拦截器,会默认拦截静态资源,需要手动放行
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/index.html","/","/user/login","/asserts/**","/webjars/**");
}
};
return adapter;
}
登陆错误消息的显示
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
4.员工CRUD
实验要求:
1)、RestfulCRUD:CRUD满足Rest风格;
URI: /资源名称/资源标识 HTTP请求方式区分对资源CRUD操作
普通CRUD(uri来区分操作) | RestfulCRUD | |
---|---|---|
查询 | getEmp | emp—GET |
添加 | addEmp?xxx | emp—POST |
修改 | updateEmp?id=xxx&xxx=xx | emp/{id}—PUT |
删除 | deleteEmp?id=1 | emp/{id}—DELETE |
2)、实验的请求架构;
实验功能 | 请求URI | 请求方式 |
---|---|---|
查询所有员工 | emps | GET |
查询某个员工(来到修改页面) | emp/1 | GET |
来到添加页面 | emp | GET |
添加员工 | emp | POST |
来到修改页面(查出员工进行信息回显) | emp/1 | GET |
修改员工 | emp | PUT |
删除员工 | emp/1 | DELETE |
3)、员工列表:
thymeleaf公共页面元素抽取
1、抽取公共片段
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>
2、引入公共片段
<div th:insert="~{footer :: copy}"></div>
~{templatename::selector}:模板名::选择器
~{templatename::fragmentname}:模板名::片段名
3、默认效果:
insert的公共片段在div标签中
如果使用th:insert等属性进行引入,可以不用写~{}:
行内写法可以加上:[[~{}]];[(~{})];
三种引入公共片段的th属性:
th:insert:将公共片段整个插入到声明引入的元素中
th:replace:将声明引入的元素替换为公共片段
th:include:将被引入的片段的内容包含进这个标签中
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
引入方式
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>
效果
<div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
<div>
© 2011 The Good Thymes Virtual Grocery
</div>
引入片段的时候传入参数:
<nav class="col‐md‐2 d‐none d‐md‐block bg‐light sidebar" id="sidebar">
<div class="sidebar‐sticky">
<ul class="nav flex‐column">
<li class="nav‐item">
<a class="nav‐link active"
th:class="${activeUri=='main.html'?'nav‐link active':'nav‐link'}"
href="#" th:href="@{/main.html}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke‐width="2" stroke‐
linecap="round" stroke‐linejoin="round" class="feather feather‐home">
<path d="M3 9l9‐7 9 7v11a2 2 0 0 1‐2 2H5a2 2 0 0 1‐2‐2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
Dashboard <span class="sr‐only">(current)</span>
</a>
</li>
<!‐‐引入侧边栏;传入参数‐‐>
<div th:replace="commons/bar::#sidebar(activeUri='emps')"></div>
4)员工添加
添加页面
<form>
<div class="form‐group">
<label>LastName</label>
<input type="text" class="form‐control" placeholder="zhangsan">
</div>
<div class="form‐group">
<label>Email</label>
<input type="email" class="form‐control" placeholder="zhangsan@atguigu.com">
</div>
<div class="form‐group">
<label>Gender</label><br/>
<div class="form‐check form‐check‐inline">
<input class="form‐check‐input" type="radio" name="gender" value="1">
<label class="form‐check‐label">男</label>
</div>
<div class="form‐check form‐check‐inline">
<input class="form‐check‐input" type="radio" name="gender" value="0">
<label class="form‐check‐label">女</label>
</div>
</div>
<div class="form‐group">
<label>department</label>
<select class="form‐control">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
</select>
</div>
<div class="form‐group">
<label>Birth</label>
<input type="text" class="form‐control" placeholder="zhangsan">
</div>
<button type="submit" class="btn btn‐primary">添加</button>
</form>
提交的数据格式不对:生日:日期;
2017-12-12;2017/12/12;2017.12.12;
日期的格式化;SpringMVC将页面提交的值需要转换为指定的类型;
2017-12-12—Date; 类型转换,格式化;
默认日期是按照/的方式;
如果想要修改:在配置文件中:
spring.mvc.date-format=yyyy-MM-dd
5)员工修改
<form th:action="@{/emp}" method="post">
<!--发送put请求修改员工数据-->
<!--
1、SpringMVC中配置HiddenHttpMethodFilter;(SpringBoot自动配置好的)
2、页面创建一个post表单
3、创建一个input项,name="_method";值就是我们指定的请求方式
-->
<input type="hidden" name="_method" value="put" th:if="${emp!=null}"/>
<input type="hidden" name="id" th:if="${emp!=null}" th:value="${emp.id}">
<div class="form-group">
<label>LastName</label>
<input name="lastName" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${emp.lastName}">
</div>
<div class="form-group">
<label>Email</label>
<input name="email" type="email" class="form-control" placeholder="zhangsan@atguigu.com" th:value="${emp!=null}?${emp.email}">
</div>
<div class="form-group">
<label>Gender</label><br/>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="1" th:checked="${emp!=null}?${emp.gender==1}">
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0" th:checked="${emp!=null}?${emp.gender==0}">
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<!--提交的是部门的id-->
<select class="form-control" name="department.id">
<option th:selected="${emp!=null}?${dept.id == emp.department.id}" th:value="${dept.id}" th:each="dept:${depts}" th:text="${dept.departmentName}">1</option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input name="birth" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}">
</div>
<button type="submit" class="btn btn-primary" th:text="${emp!=null}?'修改':'添加'">添加</button>
</form>
指定日期格式化的方式
<input name="birth" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}">
控制层
/**
* 回显
*/
@GetMapping("/emp/{id}")
public String see(@PathVariable Integer id,Model model){
Employee employee = employeeDao.get(id);
model.addAttribute("emp",employee);
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("depts",departments);
return "emp/add";
}
/**
* 修改
*/
@PutMapping("/emp")
public String update(Employee employee){
employeeDao.save(employee);
return "redirect:/emps";
}
6)员工删除
#springboot2.0以上默认关闭delete请求,需要手动开启
spring.mvc.hiddenmethod.filter.enabled=true
前端页面
<button th:attr="del_uri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger deleteBtn">删除</button>
<form id="deleteEmpForm" method="post">
<input type="hidden" name="_method" value="delete"/>
</form>
<script>
$(".deleteBtn").click(function(){
//删除当前员工的
$("#deleteEmpForm").attr("action",$(this).attr("del_uri")).submit();
return false;
});
</script>
<button th:attr="del_uri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger deleteBtn">删除</button>
控制层
/**
* 删除
*/
@DeleteMapping("/emp/{id}")
public String delete(@PathVariable("id") Integer id){
employeeDao.delete(id);
return "redirect:/emps";
}
springboot底层会将我们控制器的返回值当做页面进行渲染,如果跳转到非页面的url,应该在
//请求重定向
return "redirect:/emps";
//请求转发
return "forward:/emps";
7.错误处理机制
1)、SpringBoot默认的错误处理机制
浏览器发送消息的请求头:
2)如果是其他客户端,默认响应一个json数据
3)SpringBoot对错误请求处理的原理分析
错误处理的自动配置:ErrorMvcAutoConfiguration
这个类一共给容器中添加了以下组件:
1.DefaultErrorAttributes
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
}
点击进入DefaultErrorAttributes
//帮助我们在页面共享信息
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
2.BasicErrorController
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
点击进入BasicErrorController
//产生html类型的数据;浏览器发送的请求来到这个方法处理
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//去哪个页面作为错误页面;包含页面地址和页面内容
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping//产生json数据,其他客户端来到这个方法处理;
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
3.ErrorPageCustomizer
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
点击进入ErrorPageCustomizer
protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
点击进入getError()
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
再回到刚才的registerErrorPages方法点击getPath()
//制定了错误页面的访问路径
@Value("${error.path:/error}")
private String path = "/error";
4.DefaultErrorViewResolver
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}
点击进入DefaultErrorViewResolver
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
/**
在此处对错误页面进行视图渲染
*/
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
/**
模板引擎可以解析这个页面地址就用模板引擎解析
*/
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
/**
模板引擎可用的情况下返回到errorViewName指定的视图地址
*/
return new ModelAndView(errorViewName, model);
}
/**
模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页 面 error/404.html
*/
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
4)整体流程分析
一但系统出现4xx或者5xx之类的错误;ErrorPageCustomizer就会生效(定制错误的响应规则);就会来到/error
请求;就会被BasicErrorController处理,响应页面;去哪个页面是由DefaultErrorViewResolver解析得到的,然后有DefaultErrorAttributes帮我们在页面共享信息
5)如何定制错误的页面
1)、有模板引擎的情况下;error/状态码; 【将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的
error文件夹下】,发生此状态码的错误就会来到 对应的页面;
我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态
码.html);
页面能获取的信息;
timestamp:时间戳
status:状态码
error:错误提示
exception:异常对象
message:异常消息
errors:JSR303数据校验的错误都在这里
2)、没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找;
3)、以上都没有错误页面,就是默认来到SpringBoot默认的错误提示页面;
自定义异常处理&返回定制的json数据并转发到/error进行自适应效果处理
出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由
getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法);
1、完全来编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】,放在容器中;
2、页面上能用的数据,或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes得到;
容器中DefaultErrorAttributes.getErrorAttributes();默认进行数据处理的;
/**
* @author yinhuidong
* @createTime 2020-05-07-14:16
* 自定义异常处理器
*/
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public String handlerException(Exception e, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
/**
* 传入我们自己的错误码,4xx,5xx,否则就不会进入定制错误页面的解析流程
* getAttribute("javax.servlet.error.status_code);
*/
request.setAttribute("javax.servlet.error.status_code", 500);
map.put("code", "user.notexist");
map.put("messages", e.getMessage());
//转发到/error
return "forward:/error";
}
}
/**
* @author yinhuidong
* @createTime 2020-05-07-16:09
* 给容器中加入我们自己定义的ErrorAttributes
*/
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String,Object> map=super.getErrorAttributes(webRequest,includeStackTrace);
map.put("company","atguigu");
return map;
}
}
8.配置嵌入式Servlet容器
SpringBoot默认使用Tomcat作为嵌入式的Servlet容器;
1)如何定制和修改Servlet容器的相关配置;
1、修改和server有关的配置(ServerProperties【也是EmbeddedServletContainerCustomizer】)
server.port=8081
server.context‐path=/crud
server.tomcat.uri‐encoding=UTF‐8
//通用的Servlet容器设置
server.xxx
//Tomcat的设置
server.tomcat.xxx
2.编写一个ConfigurableServletWebServerFactory:嵌入式的Servlet容器的定制器;来修改Servlet容器的配置
/**
* @author yinhuidong
* @createTime 2020-05-07-16:52
* tomcat的配置类
*/
@Configuration
public class TomCatConfiguration {
/**
* 修改springboot对tomcat的默认配置
*/
@Bean
public ConfigurableServletWebServerFactory webServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.setPort(8083);
return factory;
}
}
原理分析
方法返回值ConfigurableServletWebServerFactory
public interface ConfigurableServletWebServerFactory extends ConfigurableWebServerFactory, ServletWebServerFactory {
void setContextPath(String contextPath);
void setDisplayName(String displayName);
void setSession(Session session);
void setRegisterDefaultServlet(boolean registerDefaultServlet);
void setMimeMappings(MimeMappings mimeMappings);
void setDocumentRoot(File documentRoot);
void setInitializers(List<? extends ServletContextInitializer> initializers);
void addInitializers(ServletContextInitializer... initializers);
void setJsp(Jsp jsp);
void setLocaleCharsetMappings(Map<Locale, Charset> localeCharsetMappings);
void setInitParameters(Map<String, String> initParameters);
}
点击进入ConfigurableWebServerFactory
public interface ConfigurableWebServerFactory extends WebServerFactory, ErrorPageRegistry {
void setPort(int port);
void setAddress(InetAddress address);
void setErrorPages(Set<? extends ErrorPage> errorPages);
void setSsl(Ssl ssl);
void setSslStoreProvider(SslStoreProvider sslStoreProvider);
void setHttp2(Http2 http2);
void setCompression(Compression compression);
void setServerHeader(String serverHeader);
}
new出来的对象TomcatServletWebServerFactory
public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory
implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware {
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final Set<Class<?>> NO_CLASSES = Collections.emptySet();
/**
* The class name of default protocol used.
*/
public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";
private File baseDirectory;
private List<Valve> engineValves = new ArrayList<>();
private List<Valve> contextValves = new ArrayList<>();
private List<LifecycleListener> contextLifecycleListeners = getDefaultLifecycleListeners();
private Set<TomcatContextCustomizer> tomcatContextCustomizers = new LinkedHashSet<>();
private Set<TomcatConnectorCustomizer> tomcatConnectorCustomizers = new LinkedHashSet<>();
private Set<TomcatProtocolHandlerCustomizer<?>> tomcatProtocolHandlerCustomizers = new LinkedHashSet<>();
private final List<Connector> additionalTomcatConnectors = new ArrayList<>();
private ResourceLoader resourceLoader;
private String protocol = DEFAULT_PROTOCOL;
private Set<String> tldSkipPatterns = new LinkedHashSet<>(TldSkipPatterns.DEFAULT);
private Charset uriEncoding = DEFAULT_CHARSET;
private int backgroundProcessorDelay;
private boolean disableMBeanRegistry = true;
ConfigurableTomcatWebServerFactory
public interface ConfigurableTomcatWebServerFactory extends ConfigurableWebServerFactory {
void setBaseDirectory(File baseDirectory);
void setBackgroundProcessorDelay(int delay);
void addEngineValves(Valve... engineValves);
void addConnectorCustomizers(TomcatConnectorCustomizer... tomcatConnectorCustomizers);
void addContextCustomizers(TomcatContextCustomizer... tomcatContextCustomizers);
void addProtocolHandlerCustomizers(TomcatProtocolHandlerCustomizer<?>... tomcatProtocolHandlerCustomizers);
void setUriEncoding(Charset uriEncoding);
}
由此可见:
new 的对象
TomcatServletWebServerFactory implements ConfigurableTomcatWebServerFactory implements ConfigurableWebServerFactory
方法返回值
ConfigurableServletWebServerFactory implements ConfigurableWebServerFactory
他们最终都指向了 ConfigurableWebServerFactory
2)注册Servlet三大组件【Servlet、Filter、Listener】
由于SpringBoot默认是以jar包的方式启动嵌入式的Servlet容器来启动SpringBoot的web应用,没有web.xml文件。
注册三大组件用以下方式:
1.ServletRegistrationBean
/**
* @author yinhuidong
* @createTime 2020-05-07-18:10
*/
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
/**
* 注册一个servlet
* @return
*/
@Bean
public ServletRegistrationBean servletRegistrationBean(){
return new ServletRegistrationBean(new MyServlet(),"/servlet");
}
2**.FilterRegistrationBean**
/**
* @author yinhuidong
* @createTime 2020-05-07-18:14
*/
public class MyFilter extends HttpFilter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("filter()......");
chain.doFilter(request,response);
}
}
/**
* 注册一个过滤器
*/
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new MyFilter());
bean.setUrlPatterns(Arrays.asList("/servlet"));
return bean;
}
3.ServletListenerRegistrationBean
/**
* @author yinhuidong
* @createTime 2020-05-07-18:22
*/
public class MyListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
System.out.println("容器创建了");
}
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("容器销毁了");
}
}
/**
* 注册一个监听器
*/
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
return new ServletListenerRegistrationBean<>(new MyListener());
}
SpringBoot帮我们自动SpringMVC的时候,自动的注册SpringMVC的前端控制器;DIspatcherServlet;
DispatcherServletAutoConfiguration中:
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
//默认拦截: / 所有请求;包静态资源,但是不拦截jsp请求; /*会拦截jsp
//可以通过server.servletPath来修改SpringMVC前端控制器默认拦截的请求路径
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
3)替换为其他嵌入式Servlet容器
默认支持:
Tomcat(默认使用)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐web</artifactId>
引入web模块默认就是使用嵌入式的Tomcat作为Servlet容器;
</dependency>
Jetty
<!‐‐ 引入web模块 ‐‐>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring‐boot‐starter‐tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!‐‐引入其他的Servlet容器‐‐>
<dependency>
<artifactId>spring‐boot‐starter‐jetty</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>
Undertow
<!‐‐ 引入web模块 ‐‐>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring‐boot‐starter‐tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!‐‐引入其他的Servlet容器‐‐>
<dependency>
<artifactId>spring‐boot‐starter‐undertow</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>
4)嵌入式Servlet容器自动配置原理
Spring Boot 2.2.6 版本的嵌入式Servlet容器自动配置是通过WebServerFactoryCustomizer定制器来定制的
@FunctionalInterface
public interface WebServerFactoryCustomizer<T extends WebServerFactory> {
/**
* Customize the specified {@link WebServerFactory}.
* @param factory the web server factory to customize
*/
void customize(T factory);
}
首先项目启动,调用ServletWebServerApplicationContext这个类中的**createWebServer()**方法:
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
/**
* 获取到一个与当前应用所导入的Servlet类型相匹配的web服务工厂定制器
*/
ServletWebServerFactory factory = getWebServerFactory();
/**
*生成Tomcat web服务工厂定制器,定制Servlet容器配置
*/
this.webServer = factory.getWebServer(getSelfInitializer());
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
该方法最终能够获取到一个与当前应用所导入的Servlet类型相匹配的web服务工厂定制器,例如你的pom文件导入的Servlet依赖为Tomcat,那么最终会生成Tomcat web服务工厂定制器,定制Servlet容器配置。
并通过上述代码中的getWebServer()方法创建对应的Servlet容器,并启动容器。
来到EmbeddedWebServerFactoryCustomizerAutoConfiguration(嵌入式web服务工厂定制器自动配置类),根据导入的依赖信息,该配置类会自动创建相应类型的容器工厂定制器(目前Spring Boot 2.x 版本支持tomcat、jetty、undertow、netty),以tomcat为例,这里会创建TomcatWebServerFactoryCustomizer组件:
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication
//倒入配置
@EnableConfigurationProperties(ServerProperties.class)
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
/**
* 容器工厂定制器
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
public static class TomcatWebServerFactoryCustomizerConfiguration {
@Bean
public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
}
}
WebServerFactoryCustomizerBeanPostProcessor(web服务工厂定制器组件的后置处理器),该类负责在bean组件初始化之前执行初始化工作。
该类先从IOC容器中获取所有类型为WebServerFactoryCustomizer(web服务工厂定制器)的组件:
private Collection<WebServerFactoryCustomizer<?>> getCustomizers() {
if (this.customizers == null) {
// Look up does not include the parent context
this.customizers = new ArrayList<>(getWebServerFactoryCustomizerBeans());
this.customizers.sort(AnnotationAwareOrderComparator.INSTANCE);
this.customizers = Collections.unmodifiableList(this.customizers);
}
return this.customizers;
}
获取到所有的定制器后,后置处理器调用定制器的**customize()**方法来给嵌入式的Servlet容器进行配置(默认或者自定义的配置):
@Override
public void customize(ConfigurableTomcatWebServerFactory factory) {
ServerProperties properties = this.serverProperties;
ServerProperties.Tomcat tomcatProperties = properties.getTomcat();
PropertyMapper propertyMapper = PropertyMapper.get();
propertyMapper.from(tomcatProperties::getBasedir).whenNonNull().to(factory::setBaseDirectory);
propertyMapper.from(tomcatProperties::getBackgroundProcessorDelay).whenNonNull().as(Duration::getSeconds)
.as(Long::intValue).to(factory::setBackgroundProcessorDelay);
customizeRemoteIpValve(factory);
propertyMapper.from(tomcatProperties::getMaxThreads).when(this::isPositive)
.to((maxThreads) -> customizeMaxThreads(factory, tomcatProperties.getMaxThreads()));
propertyMapper.from(tomcatProperties::getMinSpareThreads).when(this::isPositive)
.to((minSpareThreads) -> customizeMinThreads(factory, minSpareThreads));
propertyMapper.from(this.serverProperties.getMaxHttpHeaderSize()).whenNonNull().asInt(DataSize::toBytes)
.when(this::isPositive)
.to((maxHttpHeaderSize) -> customizeMaxHttpHeaderSize(factory, maxHttpHeaderSize));
propertyMapper.from(tomcatProperties::getMaxSwallowSize).whenNonNull().asInt(DataSize::toBytes)
.to((maxSwallowSize) -> customizeMaxSwallowSize(factory, maxSwallowSize));
propertyMapper.from(tomcatProperties::getMaxHttpFormPostSize).asInt(DataSize::toBytes)
.when((maxHttpFormPostSize) -> maxHttpFormPostSize != 0)
.to((maxHttpFormPostSize) -> customizeMaxHttpFormPostSize(factory, maxHttpFormPostSize));
propertyMapper.from(tomcatProperties::getAccesslog).when(ServerProperties.Tomcat.Accesslog::isEnabled)
.to((enabled) -> customizeAccessLog(factory));
propertyMapper.from(tomcatProperties::getUriEncoding).whenNonNull().to(factory::setUriEncoding);
propertyMapper.from(properties::getConnectionTimeout).whenNonNull()
.to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout));
propertyMapper.from(tomcatProperties::getConnectionTimeout).whenNonNull()
.to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout));
propertyMapper.from(tomcatProperties::getMaxConnections).when(this::isPositive)
.to((maxConnections) -> customizeMaxConnections(factory, maxConnections));
propertyMapper.from(tomcatProperties::getAcceptCount).when(this::isPositive)
.to((acceptCount) -> customizeAcceptCount(factory, acceptCount));
propertyMapper.from(tomcatProperties::getProcessorCache)
.to((processorCache) -> customizeProcessorCache(factory, processorCache));
propertyMapper.from(tomcatProperties::getRelaxedPathChars).as(this::joinCharacters).whenHasText()
.to((relaxedChars) -> customizeRelaxedPathChars(factory, relaxedChars));
propertyMapper.from(tomcatProperties::getRelaxedQueryChars).as(this::joinCharacters).whenHasText()
.to((relaxedChars) -> customizeRelaxedQueryChars(factory, relaxedChars));
customizeStaticResources(factory);
customizeErrorReportValve(properties.getError(), factory);
}
从源码分析可以得出配置嵌入式Servlet容器的两种解决方案:
1.在全局配置文件中,通过server.xxx来修改和server有关的配置:
server.port=8081
server.tomcat.xxx...
2、实现WebServerFactoryCustomizer接口,重写它的customize()方法,对容器进行定制配置:,但是比较麻烦,可以直接参照上面的分析:ConfigurableServletWebServerFactory
/**
* @author yinhuidong
* @createTime 2020-05-07-16:52
* tomcat的配置类
*/
@Configuration
public class TomCatConfiguration {
/**
* 修改springboot对tomcat的默认配置
*/
@Bean
public ConfigurableServletWebServerFactory webServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.setPort(8083);
return factory;
}
}
5)、嵌入式Servlet容器启动原理
分析ServletWebServerApplicationContext类的**createWebServer()**方法:
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
应用启动后,根据导入的依赖信息,创建了相应的Servlet容器工厂,以tomcat为例,创建嵌入式的Tomcat容器工厂TomcatServletWebServerFactory,调用getWebServer()方法创建Tomcat容器:
public WebServer getWebServer(ServletContextInitializer... initializers) {
//创建嵌入式的Tomcat容器
Tomcat tomcat = new Tomcat();
File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
tomcat.getService().addConnector(connector);
this.customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
this.configureEngine(tomcat.getEngine());
Iterator var5 = this.additionalTomcatConnectors.iterator();
while(var5.hasNext()) {
Connector additionalConnector = (Connector)var5.next();
tomcat.getService().addConnector(additionalConnector);
}
this.prepareContext(tomcat.getHost(), initializers);
return this.getTomcatWebServer(tomcat);
}
...
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
//调用TomcatWebServer类的有参构造器
//如果配置的端口号>=0,创建Tomcat容器
return new TomcatWebServer(tomcat, this.getPort() >= 0);
}
分析TomcatWebServer类的有参构造器:
public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
this.monitor = new Object();
this.serviceConnectors = new HashMap();
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
this.initialize();
}
执行initialize(),调用start()方法完成了tomcat容器的启动:
private void initialize() throws WebServerException {
logger.info("Tomcat initialized with port(s): " + this.getPortsDescription(false));
Object var1 = this.monitor;
synchronized(this.monitor) {
try {
this.addInstanceIdToEngineName();
Context context = this.findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && "start".equals(event.getType())) {
this.removeServiceConnectors();
}
});
//启动tomcat容器
this.tomcat.start();
this.rethrowDeferredStartupExceptions();
总结:
1、Spring Boot 根据导入的依赖信息,自动创建对应的web服务工厂定制器;
2、web服务工厂定制器组件的后置处理器获取所有类型为web服务工厂定制器的组件(包含实现WebServerFactoryCustomizer接口,自定义的定制器组件),依次调用customize()定制接口,定制Servlet容器配置;
3、嵌入式的Servlet容器工厂创建tomcat容器,初始化并启动容器。
9.使用外置的Servlet容器
嵌入式Servlet容器:应用打成可执行的jar
优点:简单、便携;
缺点:默认不支持JSP、优化定制比较复杂(使用定制器,自己编写嵌入式Servlet容器的创建工厂);
外置的Servlet容器:外面安装Tomcat—应用war包的方式打包;
步骤
1)、必须创建一个war项目;(利用idea创建好目录结构)
2)、将嵌入式的Tomcat指定为provided;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐tomcat</artifactId>
<scope>provided</scope>
</dependency>
3)、必须编写一个SpringBootServletInitializer的子类,并调用configure方法
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
//传入SpringBoot应用的主程序
return application.sources(SpringBoot04WebJspApplication.class);
}
}
4)、启动服务器就可以使用;
jar包:执行SpringBoot主类的main方法,启动ioc容器,创建嵌入式的Servlet容器;
war包:启动服务器,服务器启动SpringBoot应用【SpringBootServletInitializer】,启动ioc容器;
servlet3.0(Spring注解版):
8.2.4 Shared libraries / runtimes pluggability:
规则:
1)、服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面ServletContainerInitializer实例:
2)、ServletContainerInitializer的实现放在jar包的META-INF/services文件夹下,有一个名为
javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名
3)、还可以使用@HandlesTypes,在应用启动的时候加载我们感兴趣的类;
流程:
1)、启动Tomcat
2)、org\springframework\spring-web\4.3.14.RELEASE\spring-web-4.3.14.RELEASE.jar!\META-
INF\services\javax.servlet.ServletContainerInitializer:
Spring的web模块里面有这个文件:org.springframework.web.SpringServletContainerInitializer
3)、SpringServletContainerInitializer将@HandlesTypes(WebApplicationInitializer.class)标注的所有这个类型
的类都传入到onStartup方法的Set>;为这些WebApplicationInitializer类型的类创建实例;
4)、每一个WebApplicationInitializer都调用自己的onStartup;
5)、相当于我们的SpringBootServletInitializer的类会被创建对象,并执行onStartup方法
6)、SpringBootServletInitializer实例执行onStartup的时候会createRootApplicationContext;创建容器
protected WebApplicationContext createRootApplicationContext(
ServletContext servletContext) {
//1、创建SpringApplicationBuilder
SpringApplicationBuilder builder = createSpringApplicationBuilder();
StandardServletEnvironment environment = new StandardServletEnvironment();
environment.initPropertySources(servletContext, null);
builder.environment(environment);
builder.main(getClass());
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
builder.initializers(
new ServletContextApplicationContextInitializer(servletContext));
builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
//调用configure方法,子类重写了这个方法,将SpringBoot的主程序类传入了进来
builder = configure(builder);
//使用builder创建一个Spring应用
SpringApplication application = builder.build();
if (application.getSources().isEmpty() && AnnotationUtils
.findAnnotation(getClass(), Configuration.class) != null) {
application.getSources().add(getClass());
}
Assert.state(!application.getSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+ "configure method or add an @Configuration annotation");
// Ensure error pages are registered
if (this.registerErrorPageFilter) {
application.getSources().add(ErrorPageFilterConfiguration.class);
}
//启动Spring应用
return run(application);
}
7)、Spring的应用就启动并且创建IOC容器
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
FailureAnalyzers analyzers = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
analyzers = new FailureAnalyzers(context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
//刷新IOC容器
refreshContext(context);
afterRefresh(context, applicationArguments);
listeners.finished(context, null);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
return context;
}
catch (Throwable ex) {
handleRunFailure(context, listeners, analyzers, ex);
throw new IllegalStateException(ex);
}
}
启动Servlet容器,再启动SpringBoot应用。