分布式微服务架构之SpringBoot基础篇

    科技2022-08-18  117

    一,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环境@ConditionalOnJndiJNDI存在指定项

    自动配置类必须在一定的条件下才能生效; 我们怎么知道哪些自动配置类生效; 我们可以通过启用 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-loggingLog4j 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.filelogging.pathExampleDescription(none)(none)只在控制台输出指定文件名(none)my.log输出日志到my.log文件(none)指定目录/var/log输出到指定目录的 spring.log 文件中

    2.指定配置

    给类路径下放上每个日志框架自己的配置文件即可;SpringBoot就不使用他默认配置的了

    Logging SystemCustomizationLogbacklogback-spring.xml, logback-spring.groovy, logback.xml or logback.groovyLog4j2log4j2-spring.xml or log4j2.xmlJDK (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查询getEmpemp—GET添加addEmp?xxxemp—POST修改updateEmp?id=xxx&xxx=xxemp/{id}—PUT删除deleteEmp?id=1emp/{id}—DELETE
    2)、实验的请求架构;
    实验功能请求URI请求方式查询所有员工empsGET查询某个员工(来到修改页面)emp/1GET来到添加页面empGET添加员工empPOST来到修改页面(查出员工进行信息回显)emp/1GET修改员工empPUT删除员工emp/1DELETE
    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应用。


    Processed: 0.021, SQL: 9