SpringBoot (二) 日志入门、Web开发、Web自动配置原理

3 Log

3.1 简介

技术选型

  • 日志门面 (抽象层)
    • JCL 2014年最后更新
    • SLF4j (Simple Logging Facade for Java)
    • jboss-logging 天生不适合普通程序员用
  • 日志实现
    • JUL (java.util.logging) 生来只是怕垄断
    • Log4j
    • Log4j2 不同作者, 但太先进市场稍微小了点
    • Logback 与4j同一作者, 升级版

Spring框架默认是JCL, SpringBoot选用SLF4j + logback

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

用logback还可以少一层

使用原则

  1. 不直接调用实现类, 而是调用抽象层;
  2. 配置文件还是按照"日志实现"来配置

HelloWorld

  1. 导入slf4j的jar和logback实现的jar

  2. 官方helloworld

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

历史遗留日志的替换 (legacy)

  1. Spring(commons-logging) + Hibernate(jboss-logging) + MyBatis(xxx)

  2. 老系统不同组件可能不同日志工具

  3. 要统一日志, 即使别的框架也要统一使用slf4j

  4. 官方提供了replace的方案, 如下图. 用jcl-over-slf4j.jar来对commons logging API偷天换日

  5. “中间包"偷天换日的"先锋队”: jcl-over-slf4j.jar | log4j-over-slf4j.jar | jul-to-slf4j.jar

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

具体如何统一到slf4j?

  1. 将系统中所有其它日志框架先排除出去
  2. 用 “中间包” 来替换原来的日志工具
  3. 导入slf4j的其他实现

3.2 Spring Boot 日志关系

SpringBoot能自动适配所有日志工具 (通过各种"中间包"), 自己底层用的是slf4j + logback的日志组合. 如果引入别的组件或者开发框架, 只需要把这个框架底层依赖的日志排除掉即可!

SpringBoot 的日志

SpringBoot日志依赖关系图

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

总结:

  • Spring Boot底层也是使用slf4j+logback的方式进行日志记录
  • Spring Boot也把其他日志替换成slf4j (还依赖三个"中间包")

spring boot底层如何替换其他框架?

利用"中间包", “偷梁换柱”

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

为什么称之为中间包, 偷梁换柱包? 代码片段演示

@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";
	// 它们底层有这一类的操作...把明明是slf4j的偷偷交给你用
    static LogFactory logFactory = new SLF4JLogFactory();

	//.....

如果引入其他软件开发框架

一定要把这个框架底层默认集成的日志依赖排除掉, Spring会自动也是借鉴了SpringBoot底层的操作:

...
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-loggingartifactId>
dependency>
<dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-coreartifactId>
    <exclusions>
        <exclusion>
            <groupId>commons-logginggroupId>
            <artifactId>commons-loggingartifactId>
        exclusion>
    exclusions>
dependency>
...
  • - 排除

  • 注: commons logging是Spring默认的日志工具

3.4 日志使用

默认配置

  • 级别
  • 输出位置
  • 输出文件名

SpringBoot默认已经配置好了日志, 并且有一个默认日志级别, 称之为 root

package org.zhangcl.springboot;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;	// 注意这个包容易导错
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringBoot03LoggingApplicationTests {

	// 日志"记录器"
	Logger logger = LoggerFactory.getLogger(this.getClass());
	@Test
	void contextLoads() {

		/*
		日志的"级别", 由低到高!
		可以调整需要输出的日志级别, 可以按级别选择性输出"某个级别以及更高级别"
		配置文件可以设置级别
		 */
		logger.trace("这是trace日志");
		logger.debug("这是debug日志");
		logger.info("这是info日志");
		logger.warn("这是warn日志");
		logger.error("这是error日志");
		/**
		 * 但是我这边更改配置没有用...
		 */
	}

}

也可以手动配置, 不过我这边按照视频讲解配置没有效果

# 日志级别
logging.level.org.zhangcl=error

# 如果不指定路径, 则在当前项目根目录下
logging.file=springboot.log
# 如果指定完整路径+文件名, 则按照执行 (我的只有重新运行项目主启动类才能)
logging.file=D:/springboot.log

#在当前磁盘的根路径下创建spirng文件夹和默认spring.log文件, linux就是绝对路径, win是c盘根目录
logging.path=/spring/log

# 控制台输出的格式
logging.pattern.console=
# 指定文件中日志输出格式
logging.pattern.file=

file 和 path的配合

logging.file logging.path Example description
n n 只在控制台输出
指定 n my.log 项目根目录输出my.log
n 指定 /var/log 指定位置: spring.log

指定配置

给类路径下放上每个日志框架各自的规定的配置文件即可

规则

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识别

    • 就可以使用高级特性: 某段配置只能在某个环境下生效
    <springProfile name="dev">...springProfile>
    

3.5 切换框架

切换到log4j

  1. 在diagram图上剔除不要的: logback + log4j中间包
  2. 根据官网推荐再引入log4j-12, 底层自动也加入了log4j
  3. 再引入相关配置文件

切换到log4j2

根据boot官网, 需要和默认的starter-logging做二选一, 直接替换掉logging, 转而用starter-log4j2

4 Web

之前学的项目都是打jar包, 怎样web呢?

要解决的问题: (这一块内容可以删掉)

  • 如何导入静态资源
  • “首页”
  • 代替jsp: 模板引擎
  • 装配拓展SpringMVC
  • CRUD
  • 拦截器
  • 国际化: 中英文切换

4.1 静态资源映射规则

SpringMVC的相关配置都在WebMvcAutoconfiguration类中

ResourceProperties类

  • 可以设置与静态资源有关的属性, 比如缓存时间

    @ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
    public class ResourceProperties implements ResourceLoaderAware {	// 这个类可以设置与静态资源有关的参数
    	// ...
    }
    

规则1: /webjars/**

webjars是什么

  • Web Libraries in Jars

  • 是一种把主流静态资源封装成jar形式的技术, 比如jQuery.js文件一般是以静态资源导入, 但这里以依赖jar的方式封装一下给我们用,的一种技术

  • 官网给出最流行的几个例子:

源码分析

  • idea中搜索WebMvcAutoconfiguration类找到其中的这个方法, 截取如下:
		@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));
			}
		}
  • 符合/webjars/**的, 会去这个位置找资源addResourceLocations("classpath:/META-INF/resources/webjars/"

例子

  1. 我们以jQuery为例, 在webjars官网选择版本, maven方式, 通过maven引入后, 在依赖列表:

    意味着: 我们的classpath:/webjars下有东西了

  1. 发现了代码中描述的路径
  2. 测试: 请求localhost:8080/webjars/jquery/3.3.1/jquery.js , 可以打开.

规则2: 静态资源的文件夹

注: 正常情况下静态资源是不允许外部直接访问的!

  1. classpath:/META-INF/resources/

  2. classpath:/resources/

  3. classpath:/static/

  4. classpath:/public/

  5. 当前项目根路径(个人自己测试不行, 反正也是优先级最低, 不管了)

localhost:8080/

规则3: 欢迎页

  • 默认指定文件名: index.html

  • 默认位置: (如规则2)

  • 默认url:localhost:8080/, 第层已经映射好了, 记住这个就行

规则4: 网页图标

所有的: **/favicon.ico 都在静态资源文件下找

(个人不重视 不熟练)

自定义: 静态资源路径

原则

如果自定义, 则默认的会失效

如何定义?

操作application.properties中设置:

spring.resources.static-locations=classpath:/mylocation1/,classpath:/mylocation2/deep1/
  • 优先级: 按照以上写入顺序
  • /mylocation2/deep1中, 个人测试了mylocation2目录, 无效

4.2 模板引擎

  • 市面上: JSP | Velocity | Freemarker | Thymeleaf

  • 重点讲SB推荐的Thymeleaf

    • 语法简单\功能强大
  • 如果要识别在template目录下静态资源, 除了Controller, 额外还需要模板引擎支持, 比如Thymeleaf

使用 - 基本规则

依赖 (引用为准)

<properties>
   
   <thymeleaf.version>3.0.2.RELEASEthymeleaf.version>
   <thymeleaf-layout-dialect.version>2.1.1thymeleaf-layout-dialect.version>
properties>
...
   <dependency>
       <groupId>org.springframework.bootgroupId>
       <artifactId>spring-boot-starter-thymeleafartifactId>
   dependency>

识别位置和文件类型

  • classpath: /templates/*.html

约束

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

跳转

  1. classpath: /templates/success.html

  2. controller中添加跳转语句

        @RequestMapping("/success")
        public String success(){
            return "success";
        }
    
  3. 启动, 测试: http://localhost:8080/success

  4. 成功

使用 - th语法

  1. html文件头引入语法提示

    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
  2. 获取后台数据, 后端model传来msg

    <h1 th:text="${msg}">h1>
    

    引入css静态资源

    <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
    <link th:href="@{/css/signin.css}" rel="stylesheet">
    

语法规则

  • th:任意html标签属性 - 可以替换原生属性的值

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

解释:

  1. 片段包含 jsp:include
  2. 片段 遍历 c:forEach
  3. 条件判断 c:if
  4. 变量声明 c:set
  5. 属性修改 任意属性修改 给之前之后添加
  6. 指定html标签属性的值
  7. 标签体内文本 utext不转义特殊字符
  8. 声明片段
Simple expressions:
Variable Expressions: ${...}
Selection Variable Expressions: *{...}
Message Expressions: #{...}
Link URL Expressions: @{...}
Fragment Expressions: ~{...}

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: _

(后边的课程忽略了, 暂时不算自己的重心)

源码

怎样获取以上某些信息?

  1. 从idea左边项目列表的External Libraries中找到starter-autoconfigure

  2. 从其中的org目录找下来, 找到thymeleaf, 其中可以发现ThymeleafAutoconfigurationThymeleafProperties, 可以查看源码:

    截取properties代码:

    @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";
    
  3. 提取信息:

    • 配置文件中信息结构: spring.thymeleaf.xxx

    • 默认从classpath:/templates/中找.html文件

依赖详解

springboot该版本下默认的, 但官网文档说如果想从2版本到3可以设置

<properties>
    
    <thymeleaf.version>3.0.2.RELEASEthymeleaf.version>
    
    
    <thymeleaf-layout-dialect.version>2.1.1thymeleaf-layout-dialect.version>
properties>
...
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-thymeleafartifactId>
    dependency>

为什么layout版本也要变呢? 图来自layout的github, 有佐证:

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

缓存问题

需要手动关闭, 否则页面的改动不能及时表现出来. application.properties中

# thymeleaf缓存关闭
spring.thymeleaf.cache=false

4.3 Spring MVC 自动配置原理

官方文档地址: https://docs.spring.io/spring-boot/docs/1.5.10.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications

截取: (中文为跟着教程写的 不是官网内容)

27.1.1 Spring MVC auto-configuration

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

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

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.

  • 自动配置了ViewResolver 视图解析器 根据方法的返回值得到视图对象View, 决定如何渲染(转发or重定向)

  • ContentNegotiatingViewResolver: 组合所有的视图解析器

  • 如何定制? : 我们可以自己给容器中添加一个视图解析器; 自动将其组合进来

  • Support for serving static resources, including support for WebJars (see below).静态资源文件夹路径 webjars

  • Static index.html support. 静态首页访问

  • Custom Favicon support (see below).Automatic registration of Converter, GenericConverter, Formatter beans. 自动注册Converter Formatter

  • Converter 转换器 字段 属性 对象属性 比较接近类型的转换功能使用的组件

  • Formatter 格式化器 不同日期格式的转换

  • 它们都自动注册了, 注册条件是

    		@Bean
    		@ConditionalOnProperty(prefix = "spring.mvc", name = "date-format")
    		// 在文件中配置日期格式化的规则
    		public Formatter<Date> dateFormatter() {
    			return new DateFormatter(this.mvcProperties.getDateFormat());// 日期格式化组件
    		}
    
    • 在配置中配置了以上配置就是条件
    • 自己添加的formatter和converter只需要放入容器即可
  • Support for HttpMessageConverters (see below).

  • 消息转换器: SpringMVC用来转换http请求和响应的: User–>JSON

  • MessageConverters 说从容器中确定的; 获取所有的HttpMessageConverter

  • 如果自己给容器中添加, 只需要将自己的组件注册进容器即可, @Bean@Component都行

  • Automatic registration of MessageCodesResolver (see below). //定义错误代码生成规则

  • Automatic use of a ConfigurableWebBindingInitializer bean (see below).

  • 我们也可以配置一个ConfigurableWebBindingInitializer来代替默认的, (添加到容器)

    初始化WebDataBinder
    请求数据======JavaBean
    

扩展MVC配置 (add additional MVC configuration)

If you want to keep Spring Boot MVC features, and you just want to add additional MVC configuration (interceptors, formatters, view controllers etc.) you can add your own @Configuration class of type WebMvcConfigurerAdapter, but without @EnableWebMvc.

If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter or ExceptionHandlerExceptionResolver you can declare a WebMvcRegistrationsAdapter instance providing such components.

完全接管

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

如果想扩展MVC的配置, 比如增加 拦截器 格式化器 视图解析器等等, 可以这样做:

  1. 编写一个@Configuration类
  2. 类型为WebMvcConfigurerAdapter
  3. 不能@EnableWebMvc

特点是既保留默认配置, 又增加一些扩展

扩展MVC配置 - 举例

@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter{

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("testconfig").setViewName("/testconfig");
        /*
        测试通过
        相当于@RequestMapping("/testconfig")
        controller方法return "testconfig";
         */
    }
}

扩展MVC配置 - 原理

  1. 分析WebMvcAutoConfiguration类, 它是 SpringMVC的自动配置类

  2. 其中有个静态内部类WebMvcAutoConfigurationAdapter也是上边WebMvcConfigurerAdapter的子类, 它头上有个注解@Import(EnableWebMvcConfiguration.class)

  3. 这个注解导入的.class是同文件下的另一个静态内部类

    	@Configuration
    	public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
        	// ...
        }
    
  4. 从中注意到它的父类DelegatingWebMvcConfiguration类

    @Configuration
    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    
    	private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    
    	@Autowired(required = false) //此方法的参数需要从容器中获取
    	public void setConfigurers(List<WebMvcConfigurer> configurers) {
    		if (!CollectionUtils.isEmpty(configurers)) {
    			this.configurers.addWebMvcConfigurers(configurers);
    		}
    	}
        //...
    
    • @Autowired说明这个方法参数要从容器中获取, 也就意味着要从容器中获取所有的WebMvcConfigurer, 保存入List
    • 获取到的所有configurer会赋给本类的成员变量configurers, 这个变量就有值了!
  5. 有值了, 再去看本类的其他方法, 很多方法都是以这个成员变量为指针的, 例如:

        protected void addViewControllers(ViewControllerRegistry registry) {
            this.configurers.addViewControllers(registry);
        }
    

    进入this.configurers.addViewControllers(registry)

    	@Override
    	public void addViewControllers(ViewControllerRegistry registry) {
    		for (WebMvcConfigurer delegate : this.delegates) {
    			delegate.addViewControllers(registry);
    		}
    	}
    

    这个方法是把容器中所有WebMvcConfigurer拿来, 调用他们的addViewControllers()方法

  6. 效果:

    • 也就是容器中所有的WebMvcConfigurer都起作用, 包括我们自己写的实现
    • 但是一定不能加@EnableWebMvc, 一旦加了会"全面接管MVC配置"

全面接管SpringMVC

  • 是指SpringBoot对SpringMVC的所有自动配置不再用, 而是都靠自己配 (类似于之前SSM)
  • HOW: 在@Configuration类上添加**@EnableWebMvc**
  • 其实并不推荐

测试效果

  1. 添加之前可以直接访问静态资源 (因为有自动配置的静态资源映射)

  2. 但是添加之后不能直接访问静态资源了

  3. 说明确实会导致自动配置失效

原理

  1. 查看@EnableWebMvc

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(DelegatingWebMvcConfiguration.class)	// 注意1
    public @interface EnableWebMvc {
    }
    

    查看DelegatingWebMvcConfiguration

    @Configuration
    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {	// 注意2 父类
    
    	private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    
    
    	@Autowired(required = false)
    	public void setConfigurers(List<WebMvcConfigurer> configurers) {
    		if (!CollectionUtils.isEmpty(configurers)) {
    			this.configurers.addWebMvcConfigurers(configurers);
    		}
    	}
    
  2. 再看

    @Configuration
    @ConditionalOnWebApplication
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
    		WebMvcConfigurerAdapter.class })
    // 要求容器中没有这个Bean, 此整个类才生效
    @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
    @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
    		ValidationAutoConfiguration.class })
    public class WebMvcAutoConfiguration {
    

    此类要求没有Support类, 但是步骤1中"注意1"和"注意2"偏偏导入了它

    所以@EnableWebMvc存在的话就会导致自动配置失效

Adapter的设计思想

很妙

  1. 是一个WebMvcConfiguration接口的实现类

  2. 但是, 实现的方法全留空.

    • 假如直接实现接口, 势必要实现所有接口方法, 连不需要的也要实现, 但如果留空方法, 子类就可以间接实现接口, 就可以不必要实现所有方法, 更灵活
  3. 别的类来继承, 可以有选择性的实现!

如何修改SpringBoot的默认配置

模式

  • SpringBoot自动配置很多组件时会优先使用容器中有没有用户定制配置的(@Bean @Component)

  • 如果有些组件是多个共存, 比如ViewResolver, 则会将用户配置的+系统默认的组合使用

  • 在SpringBoot中有非常多的xxxConfigurer, 帮助我们完成扩展配置, 遇见需要多留意!

  • 在SpringBoot中会很多的xxxCustomizer 帮助我们进行定制配置

补充例子: 自定义某些功能

除了自动配置的, 如果想自定义\定制化一些功能, 可以在自己的WebMvcConfigurer实现类中定义具体子类\实现类, 然后通过@Configuration + @Bean交给Spring管理即可! 这里有一个例子:

  1. 自定义一个view resolver

    /*例如: 自定义一个view resolver
    * 写在同一个文件中只是为了方便展示*/
    public class MyViewResolver implements ViewResolver{
        @Override
        public View resolveViewName(String s, Locale locale) throws Exception {
            // ...
            return null;
        }
    }
    
  2. 交给Spring管理

    /*这里用来自定义组件并交给Spring*/
    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
    
        // 把我们自己定制的vs交给Spring
        @Bean
        public ViewResolver getMyViewResolver(){
            return new MyViewResolver();
        }
    }
    
    
    
  3. 断点debug 底层: getCandidateViews(), 获取所有候选视图时, 也会存在这个, 证明有效.

View Controller

  • 属于扩展MVC配置的一种

  • 作用是页面跳转, 代替controller一些代码

示例:

/*这里用来自定义组件并交给Spring*/
@Configuration
@EnableWebMvc
public class MyMvcConfig implements WebMvcConfigurer {

    /**
     * Configure simple automated controllers pre-configured with the response
     * status code and/or a view to render the response body. This is useful in
     * cases where there is no need for custom controller logic -- e.g. render a
     * home page, perform simple site URL redirects, return a 404 status with
     * HTML content, a 204 with no content, and more.
     *
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index").setViewName("index");
        /*拦截"/"和"/index"的请求, 转给index*/
    }
}

4.4 嵌入式Servlet容器

SpringBoot默认使用嵌入式Servlet容器: Tomcat

定制修改Servlet容器

如何定制修改Servlet容器的相关配置?

  • 方式1: 修改server有关配置, 例如:

    servet.port=8081
    server.context-path=/crud
    
    # 通用Servlet容器设置
    server.xxx
    # Tomcat的设置
    server.tomcat.xxx
    	# 例如
    server.tomcat.uri-encoding=UTF-8
    
  • 方式2: 在一个@Configuration类中编写EmbeddedServletContainerCustomizer, 通过它来修改配置

        @Bean
        public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
            return new EmbeddedServletContainerCustomizer(){
                /*定制嵌入式的Servlet容器的相关规则*/
                @Override
                public void customize(ConfigurableEmbeddedServletContainer container) {
                    container.setPort(8082);
                }
            };
        }
    

注册三大组件: Servlet、Filter、Listener

SB是jar包方式启动嵌入式Servlet容器来启动应用, 项目中没有WEB-INF/web.xml之类的文件, 让三大组件可以注册到其中, 那怎样才能注册?

其实最好的实例是SpringBoot自动注册DispatcherServlet

SpringBoot中有以下方式

  • ServletRegistrationBean
  • FilterRegistrationBean
  • ServletListenerRegistrationBean

例子 - 注册Servlet

  1. MyServlet.java

    public class Myservlet extends HttpServlet   {
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            doPost(req, resp);
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.getWriter().print("Hello, MyServlet");
        }
    }
    
  2. 注册ServletRegistrationBean

    @Configuration
    public class MyServerConfig {
        /*Servlet三大组件: Servlet*/
        @Bean
        public ServletRegistrationBean myServletRegistrationBean(){
            ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(
                new Myservlet(),"/myservlet");	// 这里用了这个类的有参构造
            return servletRegistrationBean;
        }
    }
    
  3. 请求localhost:8082/myservlet, 注意这里不小心手动改过端口号, 差点忘了.

    结果: 访问成功, 网页打印出Hello MyServlet.

例子 - Filter

  1. MyFilter.java

    public class MyFilter implements Filter {	// javax.servlet
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {}
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("MyFilter() process 生效了...");
            filterChain.doFilter(servletRequest, servletResponse);    // 假如不写这个, 就不能继续正常访问.
        }
        @Override
        public void destroy() {}
    }
    
  2. 注册在配置类中

        /*Servlet三大组件: Filter*/
        @Bean
        public FilterRegistrationBean myFilterRegistrationBean(){
            FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
            filterRegistrationBean.setFilter(new MyFilter());
            filterRegistrationBean.setUrlPatterns(Arrays.asList("/filtertest","/myservlet"));    // 需要传入Collection
            return filterRegistrationBean;
        }
    

例子 - Listener

  1. package org.zhangcl.springboot.listener;
    
    import javax.servlet.ServletContextEvent;
    import javax.servlet.ServletContextListener;
    
    public class MyListener implements ServletContextListener {
        @Override
        public void contextInitialized(ServletContextEvent servletContextEvent) {
            System.out.println("Servlet容器创建, 被我监听到了");
        }
        @Override
        public void contextDestroyed(ServletContextEvent servletContextEvent) {
            System.out.println("Servlet容器销毁, 被我监听到了");
        }
    }
    
  2.     /*Servlet三大组件: Listener*/
        @Bean
        public ServletListenerRegistrationBean myListener(){
            ServletListenerRegistrationBean<MyListener> myListenerServletListenerRegistrationBean = new ServletListenerRegistrationBean<>(new MyListener());
    //        myListenerServletListenerRegistrationBean
            return myListenerServletListenerRegistrationBean;
        }
    

注册组件 - DispatcherServlet在SpringBoot注册

DispatcherServlet在SpringBoot中注册的过程是一个非常好的学习样本

源码

在DispatcherServletAutoConfiguration类中

		@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
		@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
		public ServletRegistrationBean dispatcherServletRegistration(
				DispatcherServlet dispatcherServlet) {
			ServletRegistrationBean registration = new ServletRegistrationBean(
					dispatcherServlet, this.serverProperties.getServletMapping());
            // 拦截: "/" 包括静态资源, 但是不拦截.jsp请求  (/*会拦截.jsp)
            // 可以通过server.contextPath配置DS默认的请求路径
			registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
			registration.setLoadOnStartup(
					this.webMvcProperties.getServlet().getLoadOnStartup());
			if (this.multipartConfig != null) {
				registration.setMultipartConfig(this.multipartConfig);
			}
			return registration;
		}

切换Servlet容器

SpringBoot默认支持什么?

  • Tomcat [DEFAULT]
  • Jetty 长链接 (适合点对点聊天)
  • Undertow 高性能, 非阻塞. (支持高并发 不支持JSP)

怎么知道的?

查看ConfigurableEmbeddeServletContainer的实现类 (Ctrl + T)

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

怎样切换?

  1. 在POM依赖图中,exclude排除掉 starter-tomcat的依赖, pom中效果如下

            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
                <exclusions>
                    <exclusion>
                        <artifactId>spring-boot-starter-tomcatartifactId>
                        <groupId>org.springframework.bootgroupId>
                    exclusion>
                exclusions>
            dependency>
    
  2. POM中引入跟Tomcat依赖命名规律相同

            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-jettyartifactId>
            dependency>
    
  3. 切换之后之前设置的端口号仍然有效. 启动

    [main] .s.b.c.e.j.JettyEmbeddedServletContainer : Jetty started on port(s) 8082 (http/1.1)
    

嵌入式Servlet容器 - (自动)配置原理

  1. 查看EmbeddedServletContainerAutoConfiguration类

    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
    @Configuration
    @ConditionalOnWebApplication
    @Import(BeanPostProcessorsRegistrar.class)
    // BeanPostProcessorsRegistrar: registrar用来给容器中导入一些组件
    // 导入了EmbeddedServletContainerCustomizerBeanPostProcessor 后置处理器
    // 后置处理器: bean初始化前后, 创建完了对象, 但还没有赋值, 执行一些初始化工作!
    public class EmbeddedServletContainerAutoConfiguration {
    
        /**
    	 * Nested configuration if Tomcat is being used.
    	 */
    	@Configuration
    	@ConditionalOnClass({ Servlet.class, Tomcat.class })// 判断是否已经引入Tomcat依赖
    	@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
        // 条件生效: 没有用户自己定义的嵌入式s容器工厂 (生产这种容器的)
    	public static class EmbeddedTomcat {
    
    		@Bean
    		public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
    			return new TomcatEmbeddedServletContainerFactory();
    		}
        }    
    	/**
    	 * Nested configuration if Jetty is being used.
    	 */
    	@Configuration
    	@ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
    			WebAppContext.class })
    	@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
    	public static class EmbeddedJetty {
    
    		@Bean
    		public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
    			return new JettyEmbeddedServletContainerFactory();
    		}
    
    	}
            
            
         /**
    	 * Nested configuration if Undertow is being used.
    	 */
    	@Configuration
    	@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
    	@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
    	public static class EmbeddedUndertow {
    
    		@Bean
    		public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
    			return new UndertowEmbeddedServletContainerFactory();
    		}
    
    	}   
    }
    
  2. EmbeddedServletContainerFactory (嵌入式S容器工厂)

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

    public interface EmbeddedServletContainerFactory {
    
       /**
        * Gets a new fully configured but paused {@link EmbeddedServletContainer} instance.
        * Clients should not be able to connect to the returned server until
        * {@link EmbeddedServletContainer#start()} is called (which happens when the
        * {@link ApplicationContext} has been fully refreshed).
        * @param initializers {@link ServletContextInitializer}s that should be applied as
        * the container starts
        * @return a fully configured and started {@link EmbeddedServletContainer}
        * @see EmbeddedServletContainer#stop()
        */
       EmbeddedServletContainer getEmbeddedServletContainer(
             ServletContextInitializer... initializers);
    
    }
    
  3. EmbeddedServletContainer (…容器)

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

以Tomcat工厂为例

  • 	@Override
    	public EmbeddedServletContainer getEmbeddedServletContainer(
    			ServletContextInitializer... initializers) {
            // new 一个实例
    		Tomcat tomcat = new Tomcat();
            
            // 配置tomcat基本配置
    		File baseDir = (this.baseDirectory != null ? this.baseDirectory
    				: createTempDir("tomcat"));
    		tomcat.setBaseDir(baseDir.getAbsolutePath());
    		Connector connector = new Connector(this.protocol);	// 连接器
    		tomcat.getService().addConnector(connector);
    		customizeConnector(connector);
    		tomcat.setConnector(connector);
    		tomcat.getHost().setAutoDeploy(false);
    		configureEngine(tomcat.getEngine());
    		for (Connector additionalConnector : this.additionalTomcatConnectors) {
    			tomcat.getService().addConnector(additionalConnector);
    		}
    		prepareContext(tomcat.getHost(), initializers);
            // 配置好的实例返回
    		return getTomcatEmbeddedServletContainer(tomcat);
    	}
    
  • getTomcatEmbeddedServletContainer(tomcat);

    	/**
    	 * Factory method called to create the {@link TomcatEmbeddedServletContainer}.
    	 * Subclasses can override this method to return a different
    	 * {@link TomcatEmbeddedServletContainer} or apply additional processing to the Tomcat
    	 * server.
    	 * @param tomcat the Tomcat server.
    	 * @return a new {@link TomcatEmbeddedServletContainer} instance
    	 */
    	protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
    			Tomcat tomcat) {
    		return new TomcatEmbeddedServletContainer(tomcat, getPort() >= 0);	// 端口号若大于0则生效
    	}
    

配置是怎样生效的?

  • 配置文件: server.
  • EmbeddedServletContainerCustomizer 嵌入式S容器定制器

嵌入式Servlet容器定制器 工作原理

  1. 这个定制器有一个定制器对应的BeanPostProcessor(后置处理器)

  2. 后置处理器在AutoConfiguration类被导入进来

    ........
    @Import(BeanPostProcessorsRegistrar.class)
    // BeanPostProcessorsRegistrar: registrar用来给容器中导入一些组件
    // 导入了EmbeddedServletContainerCustomizerBeanPostProcessor 后置处理器
    // 后置处理器: bean初始化前后, 创建完了对象, 但还没有赋值, 执行一些初始化工作!
    public class EmbeddedServletContainerAutoConfiguration {
    .......
    
  3. postProcessBeforeinitialization

    	// 初始化之前
    	@Override
    	public Object postProcessBeforeInitialization(Object bean, String beanName)
    			throws BeansException {
            // 如果当前初始化的是一个ConfigurableEmbeddedServletContainer, 就调用下边的方法
    		if (bean instanceof ConfigurableEmbeddedServletContainer) {
    			postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer) bean);
    		}
    		return bean;
    	}
    
  4. 进入postProcessBeforeInitialization

    	private void postProcessBeforeInitialization(
    			ConfigurableEmbeddedServletContainer bean) {
            // 获取所有定制器, 调用每一个定制器的customize方法, 给servlet容器进行属性赋值
    		for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) {
    			customizer.customize(bean);
    		}
    	}
    
  5. getCustomizers()

    	private Collection<EmbeddedServletContainerCustomizer> getCustomizers() {
    		if (this.customizers == null) {
    			// Look up does not include the parent context
    			this.customizers = new ArrayList<EmbeddedServletContainerCustomizer>(
                    // 核心, 从容器中获取所有指定type的定制器
                    // 定制S容器, 给容器中可以添加一个嵌入式s容器定制器类型的组件
    					this.beanFactory
    							.getBeansOfType(EmbeddedServletContainerCustomizer.class,
    									false, false)
    							.values());
    			Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
    			this.customizers = Collections.unmodifiableList(this.customizers);
    		}
    		return this.customizers;
    	}
    

    核心, 从容器中获取所有指定type的定制器

  6. 从配置文件的server.进入源码, 发现ServerProperties也是一个嵌入式容器定制器的实现类

    @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
    public class ServerProperties
    		implements EmbeddedServletContainerCustomizer, EnvironmentAware, Ordered {
    
    

步骤

  1. SpringBoot根据导入的依赖情况, 给容器中添加相应的EmbeddedServletContainerFactory [比如TomcatEmbeddedServletContainerFactory]

  2. 容器中某个组件要创建对象, 就会惊动后置处理器, 使其工作

    EmbeddedServletContainerCustomizerBeanPostProcessor

    只要是嵌入式的S容器工厂, 后置处理器就会工作

  3. 后置处理器工作: 从容器中获取所有的EmServlerContainerCustomizers

  4. 然后设置tomcat的基本配置

人话:

  1. SB给容器中放入了这个工厂
  2. 只要工厂生产对象, 就会触发后置处理器
  3. 后置处理器又会获取所有定制器
  4. 定制

X Bug

嵌入式Servlet容器 - 启动原理

  • 什么时候创建嵌入式的Servlet容器工厂?

  • 什么时候获取容器并启动Tomcat?

获取嵌入式S容器工厂 (打断点获知, 屏蔽其他细节)

  1. SpringBoot应用启动, 运行run()

  2. refreshContext(context), SpringBoot刷新IoC容器 (实际就是创建IoC容器, 并初始化, 创建容器中的每一个组件 bean)

    • 如果是web应用, 就创建AnnotationConfigEmbeddedWebApplicationContext
    • 否则就创建DEAULT_CONTEXT_CLASS 也就是AnnotationConfigApplicationContext
  3. refresh(context); 刷新刚才创建好的IoC容器

    	@Override
    	public void refresh() throws BeansException, IllegalStateException {
    		synchronized (this.startupShutdownMonitor) {
    			// Prepare this context for refreshing.
    			prepareRefresh();
    
    			// Tell the subclass to refresh the internal bean factory.
    			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    
    			// Prepare the bean factory for use in this context.
    			prepareBeanFactory(beanFactory);
    
    			try {
    				// Allows post-processing of the bean factory in context subclasses.
    				postProcessBeanFactory(beanFactory);
    
    				// Invoke factory processors registered as beans in the context.
    				invokeBeanFactoryPostProcessors(beanFactory);
    
    				// Register bean processors that intercept bean creation.
    				registerBeanPostProcessors(beanFactory);
    
    				// Initialize message source for this context.
    				initMessageSource();
    
    				// Initialize event multicaster for this context.
    				initApplicationEventMulticaster();
    
                    // 主要在这一步
    				// Initialize other special beans in specific context subclasses.
    				onRefresh();
    
    				// Check for listener beans and register them.
    				registerListeners();
    
    				// Instantiate all remaining (non-lazy-init) singletons.
    				finishBeanFactoryInitialization(beanFactory);
    
    				// Last step: publish corresponding event.
    				finishRefresh();
    			}
    
    			catch (BeansException ex) {
    				if (logger.isWarnEnabled()) {
    					logger.warn("Exception encountered during context initialization - " +
    							"cancelling refresh attempt: " + ex);
    				}
    
    				// Destroy already created singletons to avoid dangling resources.
    				destroyBeans();
    
    				// Reset 'active' flag.
    				cancelRefresh(ex);
    
    				// Propagate exception to caller.
    				throw ex;
    			}
    
    			finally {
    				// Reset common introspection caches in Spring's core, since we
    				// might not ever need metadata for singleton beans anymore...
    				resetCommonCaches();
    			}
    		}
    	}
    
  4. onRefresh(); Web的IoC容器重写了onRefresh()方法

  5. webioc容器会创建嵌入式的Servlet容器 createEmbeddedServletContainer();

  6. EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();

    获取嵌入式的Servlet容器工厂

    从IoC容器中获取EmbeddedServletContainerFactory组件:

    TomcatEmbeddedServletContainerFactory 创建对象, 后置处理器一看是这个对象, 就获取所有的customizers, 来定制Servlet容器的相关配置

  7. 使用容器工厂获取嵌入式的Servlet容器: this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(getSelfInitializer());

  8. 嵌入式的Servlet容器创建对象并启动Servlet容器

    先启动嵌入式的Servlet容器, 再将IoC容器中其他未创建出的对象获取出来

    IoC容器启动, 会创建嵌入式的Servlet容器

4.5 外置Servlet容器

  • 嵌入式

    • 优点: 简单 便捷
    • 缺点:
      • 默认不支持JSP (如果非要用, 只能外置s容器)
      • 优化定制复杂 (使用定制器 ServerProperties\自定义EmbeddedServletContainerCustomizer)
      • 需要自己编写嵌入Servlet容器的创建工厂
  • 外置式 - 外部安装Tomcat, 应用打包方式war

使用

  1. 创建一个war打包项目

  2. 嵌入式的Tomcat需要指定为provided: 目标环境已经提供S容器, 打包时不需要打进来了

    <dependency>
    	<groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-tomcatartifactId>
        <scope>providedscope>
    dependency>
    
  3. 必须编写一个SpringBootServletInitializer实现类(子类)

    public class ServletInitializer extends SpringBootServletInitializer{
        @Override
        protected SpringApplicationBuilder configure(SpringApplicationBuilder application){
            // 传入SpringBoot应用主程序
            return application.sources(SpringBootWebJspApplication.class);
        }
    }
    
  4. 即可使用启动服务器

原理

(盲猜, 不是以后的重点, 暂时搁置)

你可能感兴趣的:(Java,-,框架,spring,boot,web,development,logback,slf4j)