SpringBoot入门篇学习笔记(二、日志与Web开发)

三、SpringBoot与日志

在这里插入图片描述
在这里插入图片描述
开发时,不应该直接调用日志的实现类,而是应该调用日志抽象层里面的方法。
导入slf4j的jar包和logback的实现jar包

//slf4j官方示例
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以后,配置文件还是做成日志实现框架自己本身的配置文件
当不同框架使用了不同的日志框架后,统一日志记录,可以使用slf4j提供的jar替换框架里的日志框架,实现所有框架共同使用slf4j。
在这里插入图片描述

将日志框架统一成slf4j的步骤:
  1. 将系统中的其他日志框架先排除出去
  2. 用中间包来替换原有的日志框架
  3. 导入slf4j其他的实现

1. SpringBoot的日志关系

SpringBoot使用它来做日志功能:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-logging</artifactId>
      <version>2.3.0.RELEASE</version>
      <scope>compile</scope>
    </dependency>

在这里插入图片描述
可以看到SpringBoot的日志jar包在底层的依赖关系
在这里插入图片描述
SpringBoot能自动适配所有的日志,而且底层使用slf4j+logback的方式记录日志,引入其他框架时,只需要把引入的框架依赖的日志框架去除掉。

2. 日志的关系

  1. 默认配置
    SpringBoot默认配置好了日志。
    测试log,测试类
package com.angenin.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(getClass());

    @Test
    void contextLoads() {
        //日志的级别:
        //由低到高  trace < debug < info < warn < error
        //可以调整输出(打印)的日志级别,低级别的日志就不会输出

        //跟踪
        logger.trace("trace日志。。");
        //debug
        logger.debug("debug日志。。");
        //自定义,SpringBoot默认使用info级别(root级别),即上面两个即使写在方法里也不会输出打印
        //可以在application.properties加入 logging.level.包名=trace 属性指定包下使用的日志输出级别
        logger.info("info日志。。");
        //警告
        logger.warn("warn日志。。");
        //错误
        logger.error("error日志。。");
    }
}

在application.properties中

#指定包下使用的日志输出级别
logging.level.com.angenin=trace
#在指定的路径下生成日志文件(在当前项目的根目录下生成spring/log/spring.log日志文件)
logging.file.path=spring/log
#在控制台输入的日志格式
logging.pattern.console=
#指定生成的日志文件中的输出格式
logging.pattern.file=

日志输出格式:
在这里插入图片描述

  1. 指定配置
    logback指定配置文件:在resources下新建logback.xml或logback-spring.xml文件,SpringBoot就不会用底层的默认配置,而是使用resources下的配置文件。
    在这里插入图片描述
    SpringBoot推荐我们配置文件名写为logback-spring.xml。
    logback.xml:会直接被日志框架识别
    logback-spring.xml:不会被日志框架直接加载,而是由SpringBoot解析日志配置,所以在配置文件中可使用SpringBoot提供的< springProfile >标签(< springProfile >标签可以指定某段配置文件只在某个环境下生效)。
<!--官方的示例-->
<springProfile name="staging">
	<!--在staging环境下生效-->
    <!-- configuration to be enabled when the "staging" profile is active -->
</springProfile>

<springProfile name="dev | staging">
	<!--在dev和staging环境下生效-->
    <!-- configuration to be enabled when the "dev" or "staging" profiles are active -->
</springProfile>

<springProfile name="!production">
	<!--在不是production环境下生效,production环境下不生效-->
    <!-- configuration to be enabled when the "production" profile is not active -->
</springProfile>

四、SpringBoot与Web开发

1. SpringBoot对静态资源的映射规则

WebMvcAutoConfiguration.class:

        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");
            } else {
                Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
                CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
                if (!registry.hasMappingForPattern("/webjars/**")) {
                    this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
                }

                String staticPathPattern = this.mvcProperties.getStaticPathPattern();
                if (!registry.hasMappingForPattern(staticPathPattern)) {
                    this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
                }

            }
        }
@ConfigurationProperties(
    prefix = "spring.resources",
    ignoreUnknownFields = false
)
public class ResourceProperties {...}
  1. 所有访问/webjars/**,都去classpath:/META-INF/resources/webjars/下找资源
    webjars:以jar包的方式引入静态资源

       <!--   引入jquery-webjar     -->
       <!--  在访问的时候只需要写webjars下面资源的名称即可   -->
       <dependency>
           <groupId>org.webjars</groupId>
           <artifactId>jquery</artifactId>
           <version>3.4.1</version>
       </dependency>
    

    在这里插入图片描述
    访问JQuery的路径:http://localhost:8080/webjars/jquery/3.4.1/jquery.js
    在这里插入图片描述

  2. /**访问当前项目的任何资源,会到下面5个目录(静态资源目录)找资源:

    classpath:/		为resources目录下
    "classpath:/META-INF/resources/"	优先级最高
    "classpath:/resources/"
    "classpath:/static/"
    "classpath:/public/"
    "/"				当前项目的根目录		优先级最低
    

    如果在resources/static下有a.html页面,访问的路径:http://localhost:8080/a.html
    在这里插入图片描述

  3. 欢迎页,5个静态资源目录下的所有index.html,被/**路径映射。
    下面是欢迎页的SpringBoot源码
    WebMvcAutoConfiguration.class

        @Bean
        public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
            WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
            welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
            welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
            return welcomePageHandlerMapping;
        }
    
            private Optional<Resource> getWelcomePage() {
            String[] locations = WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations());
            return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
        }
    
        private Resource getIndexHtml(String location) {
            return this.resourceLoader.getResource(location + "index.html");
        }
    

    WelcomePageHandlerMapping.class

    	WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
    	        if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
    	            logger.info("Adding welcome page: " + welcomePage.get());
    	            this.setRootViewName("forward:index.html");
    	        } else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
    	            logger.info("Adding welcome page template: index");
    	            this.setRootViewName("index");
    	        }
    	
    	    }
    

    如果5个路径静态资源路径都有index.html,会使用resources/META-INF/resources/index.html
    在这里插入图片描述

  4. 页面图标
    在静态资源下放置名为favicon.ico的图标图片,打开项目后就会在页面标签上显示。
    制作ico图标:https://tool.lu/favicon/
    在这里插入图片描述
    在这里插入图片描述

2. 模板引擎

JSP、Velocity、Freemarker、Thymeleaf,…
在这里插入图片描述
SpringBoot推荐Thymeleaf,语法简单,功能强大。

1. 引入thymeleaf
<!--    引入thymeleaf模板引擎    -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2. Thymeleaf的使用
@ConfigurationProperties(
    prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING;
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";
    private boolean checkTemplate = true;
    private boolean checkTemplateLocation = true;
    private String prefix = "classpath:/templates/";
    private String suffix = ".html";
    private String mode = "HTML";
    ...
只要把html页面放在classpath:/templates/(即resources/templates/)目录下,thymeleaf就能自动渲染。

例:
@Controller
Controller里添加:

  @RequestMapping("/success")
  public String success(){
      return "success";
  }
然后在resources/templates目录下新建一个success.html,当页面访问/success时,就会去resources/templates找返回的字符串加.html页面。

在这里插入图片描述
在这里插入图片描述
Thymeleaf官网:https://www.thymeleaf.org/index.html
右键PDF,点击链接存储为,下载pdf文档。
在这里插入图片描述
在html页面的html标签中导入thymeleaf的名称空间:

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

导入后,在使用的时候会有语法提示

修改Controller中的方法,保存数据

    //查出数据,在页面显示
   @RequestMapping("/success")
   public String success(Map<String, Object> map){
       map.put("hello", "<h1>你好</h1>");
       map.put("users", Arrays.asList("zhangsan", "lisi", "wangwu"));
       return "success";
   }

在页面中取值

    <div id="div01" class="myDiv" th:id="${hello}" th:class="${hello}" th:text="${hello}"></div>
    <hr/>
    <!--  转义特殊字符(即照原样的字符串显示)  -->
    <div th:text="${hello}"></div>
    <!--  不转义特殊字符  -->
    <div th:utext="${hello}"></div>
    <hr/>
    <!--  th:each每次遍历都会生成当前标签  -->
    <!--  写法一  -->
    <h4 th:each="user:${users}" th:text="${user}"></h4>
    <hr/>
    <!--  写法二  -->
    <h4>
        <span th:each="user:${users}">[[${user}]] </span>
    </h4>

在这里插入图片描述

3. Thymeleaf的语法

内容过多,单独写在另一篇了,详情查看:https://blog.csdn.net/qq_36903261/article/details/106190445

thymeleaf标签的优先级:
在这里插入图片描述
使用Thymeleaf的一个练习:https://blog.csdn.net/qq_36903261/article/details/106217848

3. 错误处理机制

SpringBoot默认的错误处理机制
默认效果:
  1. 如果是用浏览器进行访问的,SpringBoot会返回一个默认的错误页面
    在这里插入图片描述
    浏览器发送请求的请求头:
    在这里插入图片描述

  2. 如果是其他客户端,默认响应一个json数据的错误信息
    在这里插入图片描述
    发送请求的请求头:
    在这里插入图片描述

原理:

参照ErrorMvcAutoConfiguration(错误处理的自动配置)
给容器中添加了以下组件:

  1. DefaultErrorAttributes:帮我们在页面共享信息

    @Deprecated
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap();
        errorAttributes.put("timestamp", new Date());
        this.addStatus(errorAttributes, webRequest);
        this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
        this.addPath(errorAttributes, webRequest);
        return errorAttributes;
    }
    
  2. BasicErrorController:处理默认/error请求

    @Controller
    @RequestMapping({"${server.error.path:${error.path:/error}}"})
    public class BasicErrorController extends AbstractErrorController {
    
    //产生html类型的数据,即浏览器发送的请求来到这个方法处理
    @RequestMapping(produces = {"text/html"})
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = this.getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        //去哪个页面作为错误页面,包含页面地址和页面内容
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }
    
    //产生json数据,其他客户端来到这个方法处理
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = this.getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity(status);
        } else {
            Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
            return new ResponseEntity(body, status);
        }
    }
    
  3. ErrorPageCustomizer

    @Value("${error.path:/error}")
    private String path = "/error";	//系统出现错误以后来到error请求进行处理(类似于web.xml注册的错误页面规则)
    
  4. DefaultErrorViewResolver

        public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
            ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
            if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
                modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
            }
    
            return modelAndView;
        }
    
        private ModelAndView resolve(String viewName, Map<String, Object> model) {
        	//默认SpringBoot可以去找到一个页面(error/404)
            String errorViewName = "error/" + viewName;
            //如果模板引擎可以解析这个页面地址就用模板引擎进行解析
            TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
            //模板引擎可用就返回errorViewName指定的视图地址,如果不可用,就在静态资源目录下找errorViewName对应的页面(error/404.html)
            return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
        }
    
步骤:

一旦系统出现4xx或者5xx之类的错误,ErrorPageCustomizer就会生效(定制错误的响应规则),就会来到/error请求,然后被BasicErrorController处理。
响应页面:去哪个页面由DefaultErrorViewResolver解析得到的。

    protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
       Iterator var5 = this.errorViewResolvers.iterator();

	//所有的errorViewResolvers得到modelAndView
       ModelAndView modelAndView;
       do {
           if (!var5.hasNext()) {
               return null;
           }

           ErrorViewResolver resolver = (ErrorViewResolver)var5.next();
           modelAndView = resolver.resolveErrorView(request, status, model);
       } while(modelAndView == null);

       return modelAndView;
   }
定制错误响应
1. 定制错误页面
  1. 有模板引擎的情况下:error/状态码页面(templates/error)
    将错误页面命名为错误状态码.html放在模板引擎目录里的error目录下,发生错误就会找此目录下找相对应的错误页面。
    我们也可以用4xx和5xx页面作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找相对应的错误页面,没有才找xx页面)

页面能获取的信息:

  • timestamp:时间戳
  • status:状态码
  • error:错误提示
  • exception:异常对象
  • message:异常消息
  • errors:JSR303数据校验的所有错误
  1. 没有模板引擎的情况下:模板引擎找不到这个错误页面,就会在静态资源目录下找(templates目录下没有error),但是在静态资源目录下的页面就不能使用模板引擎语法。
  2. 以上都没有错误页面,使用默认的错误提示页面。
2. 定制错误的json数据
  1. 自定义异常处理和返回定制json数据(没有自适应效果)

    package com.angenin.springboot.controller;
    
    import com.angenin.springboot.exception.UserNotExistException;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import java.util.HashMap;
    import java.util.Map;
    
    @ControllerAdvice   //异常处理器
    public class MyExceptionHandler {
    
    //1. 浏览器和其他客户端都返回json数据
    //只要出现异常SpringMVC就会调用这个方法
    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map<String, Object> handleExcption(Exception e){
        Map<String, Object> map = new HashMap<>();
        map.put("code", "user.notexist");
        map.put("message", e.getMessage());
        return map;
    }
    

    在这里插入图片描述

  2. 转发到error进行自适应效果处理

    //2. 转发到/error让BasicErrorController帮我们自适应
    @ExceptionHandler(UserNotExistException.class)
    public String handleExcption(Exception e, HttpServletRequest request){
        Map<String, Object> map = new HashMap<>();
    
        //传入我们自己的错误状态码 4xx 5xx(如果状态码不设置,转发后状态码为200)
    //        BasicErrorController中获取状态码的语句:Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");
        request.setAttribute("javax.servlet.error.status_code", 400);
    
        map.put("code", "user.notexist");
        //但是把e.getMessage()改为自定义错误信息(如用户不存在),数据却没携带出去,具体看第三点
        map.put("message", e.getMessage());
        //因为BasicErrorController是处理/error的,并且区分了浏览器和其他客户端
        //因此,只要处理完后转发到/error后,BasicErrorController就会帮我们处理
        return "forward:/error";
    }
    

    在这里插入图片描述

  3. 将我们定制的数据携带出去(第二点的扩展,重点)
    出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes(是AbstractErrorController(ErrorController)规定的方法)得到的,而注册BasicErrorController时,有@ConditionalOnMissingBean(value = {ErrorController.class},search = SearchStrategy.CURRENT)注解(即如果有ErrorController或其子类,就不注册BasicErrorController)。

    1. 我们可以完全编写一个ErrorController的实现类(或者编写AbstractErrorController的子类),重写需要的方法,放到容器中来代替BasicErrorController。
    2. 页面上能用的数据,或者json返回能用的数据都是通过errorAttributes.getErrorAttributes得到的。
      容器中DefaultErrorAttributes.getErrorAttributes()默认进行数据处理的。

    新建一个继承DefaultErrorAttributes的子类(新建ErrorAttributes的实现类也可以,不过需要直接写方法,比较麻烦)

    package com.angenin.springboot.component;
    
    import org.springframework.boot.web.error.ErrorAttributeOptions;
    import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.WebRequest;
    import java.util.Map;
    
    @Component  //给容器中加入我们自己定义的MyErrorAttributes
    public class MyErrorAttributes extends DefaultErrorAttributes {
    
        @Override
        public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
            //调用父类的方法,获取map
            Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
            //往map中添加数据
            errorAttributes.put("k1", "v1");
            //返回值的map就是页面和json能获取的所有字段
            return errorAttributes;
        }
    }
    

    我们就可以MyExceptionHandler设置完map后保存到request域中

      //保存到request域中
      request.setAttribute("ext", map);
    

    然后在MyErrorAttributes中获取request域中的map,进行保存

        @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        //调用父类的方法
        Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
        errorAttributes.put("k1", "v1");
        //webRequest 继承了RequestAttributes(RequestAttributes包装了request和session)
        //getAttribute第一个参数为保存的数据的key,第二个是从哪个域中拿数据(0为request,1为session)
        //异常处理器携带的数据,因为是异常处理器是转发到/error,所以保存到request也可以拿到,如果是重定向,那么异常处理器的数据就要保存到sesssion中
        Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
        //把获取的数据放入
        errorAttributes.put("ext", ext);
        //返回浏览器和其他客户端能读取的数据
        return errorAttributes;
    }
    

    在页面就可以用${ext.xxx}取出自己设置的信息。

    <h2>exception: [[${ext.exception}]]</h2>
    <h2>message: [[${ext.message}]]</h2>
    

    在这里插入图片描述
    最终效果:响应是自适应的,可以通过定制ErrorAttributes改变需要返回的内容。

4. SpringMVC自动配置

1.SpringMVC auto-configuration

SpringBoot对SpringMVC的默认配置:

  1. 自动注册了ContentNegotiatingViewResolverBeanNameViewResolver组件
    自动配置了ViewResolver(视图解析器:根据方法的返回值得到视图对象(View),视图对象决定如何渲染(转发/重定向/…))
    ContentNegotiatingViewResolver:组合了所有的视图解析器。
    我们可以往容器添加一个自己配置的视图解析器(实现了ViewResolver的类),SpringBoot会自动将其组合进来。
  2. 自动注册了ConverterGenericConverterFormatter组件
    Converter:转换器(类型转换)
    Formatter:格式化器(例:字符串转为日期类型(日期格式化的规则))
    我们添加格式化器和转换器,只需要放到容器中即可使用。
  3. 自动注册了HttpMessageConverters组件
    HttpMessageConverters:SpringMVC用来转换Http请求和响应的(User对象–>JSON

    HttpMessageConverters:是从容器中确定的,获取所有的HttpMessageConverter。
    给容器中添加HttpMessageConverter,即可使用。
  4. 自动注册了MessageCodesResolver组件。
    定义错误代码生成规则。
  5. 自动注册了ConfigurableWebBindingInitializer组件
    初始化WebDataBinder(web数据绑定器)(请求数据–>JavaBean)
    我们可以配置一个ConfigurableWebBindingInitializer来替换默认的,只需要添加到容器中。
  6. org.springframework.boot.autoconfigure.web:web的所有自动场景。
2. 扩展SpringMVC

即可以保留所有的自动配置,也能用我们扩展的配置。
编写一个配置类(@Configuration),实现WebMvcConfigurer,并且不能标注@EnableWebMvc。

package com.angenin.springboot.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/angenin").setViewName("success");
    }
}

在这里插入图片描述
原理:

  1. WebMvcAutoConfiguration是SpringMVC的自动配置类
  2. 在做其他自动配置时会导入@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
     @Configuration(proxyBeanMethods = false)
    public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {...}
    
    其父类的setConfigurers方法:
    private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    //从容器中获取所有的WebMvcConfigurer
    @Autowired(required = false)
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addWebMvcConfigurers(configurers);
        }
    }
    
  3. 容器中的所有WebMvcConfigurer都会一起起作用
  4. 我们的配置类也会被调用
    效果:SpringMVC 的自动配置和我们的扩展配置都会起作用。
3. 全面接管SpringMVC

SpringBoot对SpringMVC的自动配置不需要了,所有SpringMVC的自动配置都失效了,需要我们自己配置。
在配置类上添加@EnableWebMvc即可。

为什么加了@EnableWebMvc自动配置就全部失效了?
原理:

  1. @EnableWebMvc的核心
    @Import({DelegatingWebMvcConfiguration.class})
    public @interface EnableWebMvc {}
    
  2. DelegatingWebMvcConfiguration.class
    @Configuration(proxyBeanMethods = false)
    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {...}
    
  3. WebMvcAutoConfiguration.class
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
    //容器中没有这个组件的时候,这个自动配置类才生效
    @ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
    @AutoConfigureOrder(-2147483638)
    @AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
    public class WebMvcAutoConfiguration {
    
  4. @EnableWebMvc将WebMvcConfigurationSupport组件添加到容器中。
  5. 添加的WebMvcConfigurationSupport组件只是SpringMVC最基本的功能。

5. 如何修改SpringBoot的默认配置

模式:

  1. SpringBoot在自动配置很多组件的时候,会先查看容器中是否有用户自己配置的组件(@Bean/@Component/…),如果有,就使用用户配置的组件,如果没有,才自动配置。如果有些组件可以有多个(如ViewResolver),SpringBoot会将用户配置的和默认配置的组合起来。
  2. 在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置。
  3. 在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置。

6. 配置嵌入式Servlet容器

SpringBoot默认使用Tomcat作为嵌入的Servlet容器。
在这里插入图片描述

定制和修改Servlet容器相关的配置
  1. 修改和server有关的配置(ServerProperties)
    #通用的servlet容器设置 server.xxx
    server.port=8081
    server.servlet.context-path=/crud
    
    #tomcat的设置 server.tomcat.xxx
    server.tomcat.uri-encoding=utf-8
    
  2. 在@Configuration配置类中编写一个返回WebServerFactoryCustomizer的类(嵌入式的servlet容器的定制器,用于修改servlet容器的配置)
        //定制嵌入式的Servlet容器相关的规则
    @Bean
    public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){
        return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
            @Override
            public void customize(ConfigurableWebServerFactory factory) {
            	//修改端口
                factory.setPort(8090);
            }
        };
    }
    

两种方法同时写,第二种方法生效。

注册Servlet三大组件「Servlet、Filter、Listener」

由于SpringBoot默认是以jar包的方式启动嵌入式的Servlet容器来启动SpringBoot的web应用,没有web.xml文件。
所以,注册三大组件的需要用以下的方式:

  • ServletRegistrationBean
  • FilterRegistrationBean
  • ServletListenerRegistrationBean
注册Servlet(ServletRegistrationBean)

新建一个Servlet

package com.angenin.springboot.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

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().write("hello myserlvet");
    }
}

在@Configuration配置类中

   @Bean   //注册servlet
    public ServletRegistrationBean myServlet(){
		//有参构造器,第一个参数为servlet,第二个为拦截的路径
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new MyServlet(), "/myServlet");
        return servletRegistrationBean;
    }

在这里插入图片描述

注册Filter(FilterRegistrationBean)

新建一个Filter

package com.angenin.springboot.filter;

import javax.servlet.*;
import java.io.IOException;

public class MyFilter implements Filter {
    @Override   //filter的初始化
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override   //filter的过滤
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        servletResponse.getWriter().write("hello filter\n");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override   //filter的销毁
    public void destroy() {

    }
}

在@Configuration配置类中

    @Bean   //注册filter
    public FilterRegistrationBean myFilter(){
        //有参构造器,第一个参数为filter,第二个为拦截的servlet(多个)
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        //设置filter
        filterRegistrationBean.setFilter(new MyFilter());
        //设置拦截路径(多个)
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/hello", "/myServlet"));
        return filterRegistrationBean;
    }

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

注册Listener(ServletListenerRegistrationBean)

新建一个Listener

package com.angenin.springboot.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class MyListener implements ServletContextListener {

    @Override   //初始化,代表web应用启动
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("contextInitialized。。。web应用启动");
    }

    @Override   //销毁,代表web应用停止
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("contextDestroyed。。。web应用停止");
    }
}

在@Configuration配置类中

    @Bean   //注册listener
    public ServletListenerRegistrationBean myListener(){
        ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean(new MyListener());
        return servletListenerRegistrationBean;
    }

在这里插入图片描述

SpringBoot帮我们自动配置SpringMVC的时候,自动注册SpringMVC的前端控制器(DispatcherServlet)
在DispatcherServletAutoConfiguration.class中

        @Bean(name = {"dispatcherServletRegistration"})
        @ConditionalOnBean(value = {DispatcherServlet.class}, name = {"dispatcherServlet"})
        public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
        	//点进去DispatcherServletRegistrationBean可以发现是继承于ServletRegistrationBean
        	//dispatcherServlet为SpringMVC的前端控制器,xx.getPath()点进去发现是为path="/"(即拦截静态资源但不会拦截jsp,"/*"拦截静态资源和jsp)
        	//可以通过spring.mvc.servlet.path来修改SpringMVC前端控制器默认拦截的请求路径
            DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
            registration.setName("dispatcherServlet");
            registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
            multipartConfig.ifAvailable(registration::setMultipartConfig);
            return registration;
        }

我们可以通过spring.mvc.servlet.path来修改SpringMVC前端控制器默认拦截的请求路径。

切换Serlvet容器

SpringBoot还支持Jetty(适合长连接(如聊天))、Undertow(不支持jsp,高性能非阻塞,并发好)这两个servlet容器。

默认使用Tomcat
在这里插入图片描述

切换成Jetty

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!--排除tomcat-->
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>
		<!--引入jetty-->
        <dependency>
            <artifactId>spring-boot-starter-jetty</artifactId>
            <groupId>org.springframework.boot</groupId>
        </dependency>

运行程序
在这里插入图片描述
同理,切换成Undertow也一样

		<!--排除tomcat-->
        <!--引入undertow-->
        <dependency>
            <artifactId>spring-boot-starter-undertow</artifactId>
            <groupId>org.springframework.boot</groupId>
        </dependency>

运行程序
在这里插入图片描述

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

EmbeddedWebServerFactoryCustomizerAutoConfiguration:ServletWeb服务器工厂自动配置

@Configuration(
    proxyBeanMethods = false
)
@AutoConfigureOrder(-2147483648)
@ConditionalOnClass({ServletRequest.class})
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@EnableConfigurationProperties({ServerProperties.class})
@Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, EmbeddedTomcat.class, EmbeddedJetty.class, EmbeddedUndertow.class})
public class ServletWebServerFactoryAutoConfiguration {

    @Bean
    @ConditionalOnClass(
        name = {"org.apache.catalina.startup.Tomcat"}
    )
    public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(ServerProperties serverProperties) {
        return new TomcatServletWebServerFactoryCustomizer(serverProperties);
    }

ServletWebServerFactoryConfiguration:ServletWeb服务器工厂配置

    @Configuration(
    proxyBeanMethods = false
    )
    @ConditionalOnClass({Servlet.class, Undertow.class, SslClientAuthMode.class})
    @ConditionalOnMissingBean(
        value = {ServletWebServerFactory.class},
        search = SearchStrategy.CURRENT
    )
    static class EmbeddedUndertow {
        EmbeddedUndertow() {
        }

        @Bean
        UndertowServletWebServerFactory undertowServletWebServerFactory(ObjectProvider<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers, ObjectProvider<UndertowBuilderCustomizer> builderCustomizers) {
            UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
            factory.getDeploymentInfoCustomizers().addAll((Collection)deploymentInfoCustomizers.orderedStream().collect(Collectors.toList()));
            factory.getBuilderCustomizers().addAll((Collection)builderCustomizers.orderedStream().collect(Collectors.toList()));
            return factory;
        }
    }

    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnClass({Servlet.class, Server.class, Loader.class, WebAppContext.class})
    @ConditionalOnMissingBean(
        value = {ServletWebServerFactory.class},
        search = SearchStrategy.CURRENT
    )
    static class EmbeddedJetty {
        EmbeddedJetty() {
        }

        @Bean
        JettyServletWebServerFactory JettyServletWebServerFactory(ObjectProvider<JettyServerCustomizer> serverCustomizers) {
            JettyServletWebServerFactory factory = new JettyServletWebServerFactory();
            factory.getServerCustomizers().addAll((Collection)serverCustomizers.orderedStream().collect(Collectors.toList()));
            return factory;
        }
    }

    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnClass({Servlet.class, Tomcat.class, UpgradeProtocol.class})//判断当前是否引入了Tomcat依赖
    @ConditionalOnMissingBean(
        value = {ServletWebServerFactory.class},
        search = SearchStrategy.CURRENT
    )//判断当前容器是否有用户自己定义的ServletWebServerFactory(ServletWeb服务器工厂)
    static class EmbeddedTomcat {
        EmbeddedTomcat() {
        }

        @Bean
        TomcatServletWebServerFactory tomcatServletWebServerFactory(ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers, ObjectProvider<TomcatContextCustomizer> contextCustomizers, ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
            TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
            factory.getTomcatConnectorCustomizers().addAll((Collection)connectorCustomizers.orderedStream().collect(Collectors.toList()));
            factory.getTomcatContextCustomizers().addAll((Collection)contextCustomizers.orderedStream().collect(Collectors.toList()));
            factory.getTomcatProtocolHandlerCustomizers().addAll((Collection)protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
            return factory;
        }
    }

ServletWebServerFactory(ServletWeb服务器工厂)

@FunctionalInterface
public interface ServletWebServerFactory {
	//获取web服务器
    WebServer getWebServer(ServletContextInitializer... initializers);
}

以TomcatServletWebServerFactory为例:

    public WebServer getWebServer(ServletContextInitializer... initializers) {
        if (this.disableMBeanRegistry) {
            Registry.disableRegistry();
        }
		//创建一个tomcat
        Tomcat tomcat = new Tomcat();
        //配置tomcat的基本环境
        File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
        tomcat.setBaseDir(baseDir.getAbsolutePath());
        Connector connector = new Connector(this.protocol);
        connector.setThrowOnFailure(true);
        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);
        //返回配置好的tomcat
        return this.getTomcatWebServer(tomcat);
    }
嵌入式servlet容器启动原理
  1. SpringBoot应用启动运行run方法
  2. refreshContext(context);SpringBoot刷新IOC容器(创建IOC容器对象并初始化容器,创建容器中的每一个组件(如果是servlet就创建AnnotationConfigServletWebServerApplicationContext,否则如果是reactive就创建AnnotationConfigReactiveWebServerApplicationContext,如果都不是创建AnnotationConfigApplicationContext))
  3. refresh((ApplicationContext) context);刷新刚才创建好的IOC容器
    //刷新IOC容器
    public void refresh() throws BeansException, IllegalStateException {
        synchronized(this.startupShutdownMonitor) {
            this.prepareRefresh();
            ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
            this.prepareBeanFactory(beanFactory);

            try {
                this.postProcessBeanFactory(beanFactory);
                this.invokeBeanFactoryPostProcessors(beanFactory);
                this.registerBeanPostProcessors(beanFactory);
                this.initMessageSource();
                this.initApplicationEventMulticaster();
                this.onRefresh();
                this.registerListeners();
                this.finishBeanFactoryInitialization(beanFactory);
                this.finishRefresh();
            } catch (BeansException var9) {
                if (this.logger.isWarnEnabled()) {
                    this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
                }

                this.destroyBeans();
                this.cancelRefresh(var9);
                throw var9;
            } finally {
                this.resetCommonCaches();
            }

        }
    }
  1. onRefresh();servlet的IOC容器重写了onRefresh()方法
  2. servletIOC容器会创建Web服务器createWebServer();
  3. 获取ServletWeb服务器工厂ServletWebServerFactory factory = getWebServerFactory();
    从IOC容器中获取ServletWebServerFactory.class组件(即TomcatServletWebServerFactory)String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
  4. 使用容器工厂获取web服务器this.webServer = factory.getWebServer(getSelfInitializer());
  5. webServer创建对象并启动servlet容器this.tomcat.start();
    先启动嵌入式的servlet容器,再将ioc容器中剩下没创建出的对象获取出来this.finishBeanFactoryInitialization(beanFactory);

在IOC容器启动时创建嵌入式servlet容器。

使用外置的servlet容器

嵌入式servlet容器
使用:jar
优点:简单、快捷
缺点:默认不支持jsp、优化定制比较复杂

使用外置的servlet容器步骤:
新建项目,选择war包
在这里插入图片描述
只选个springweb模块,创建项目
选择war包,tomcat的依赖默认是provided
在这里插入图片描述
而新建的war包项目多了两个类(必须的),而且ServletInitializer必须继承SpringBootServletInitializer,并调用configure方法。
在这里插入图片描述
在这里插入图片描述
新建的项目是没有webapp包的
在这里插入图片描述
可以手动建,也可以自动生成
在这里插入图片描述
点击yes创建
在这里插入图片描述
然后生成web.xml文件
在这里插入图片描述
创建服务器
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然后点击应用确定,完成创建服务器
在webapp目录下新建一个hello.jsp页面,启动服务器
在这里插入图片描述
浏览器输入:http://localhost:8080/jsp/hello.jsp访问hello页面
在这里插入图片描述
和SpringMVC结合起来使用

  • 在hello.jsp中加入一个a标签<a href="abc">abc</a>
  • 在WEB-INF下新建一个success.jsp
<h1>success</h1>
<h3>${msg}</h3>
  • main/java/com/angenin/springboot下新建controller/HelloController.java
@Controller
public class HelloController {
    @GetMapping("/abc")
    public String hello(){
        model.addAttribute("msg", "你好");
        return "success";
    }
}

然后在application.properties配置文件中添加

#配置视图解析器
spring.mvc.view.prefix=/WEB-INF/
spring.mvc.view.suffix=.jsp

启动项目,从hello页面点击abc跳转到success页面
在这里插入图片描述

原理:

jar包:执行SpringBoot主类的main方法,启动ioc容器,创建嵌入式的servlet容器。
war包:启动服务器,服务器启动SpringBoot应用(关键在于SpringBootServletInitializer),启动ioc容器。

规则:
  1. 服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面ServletContainerInitializer实例。
  2. ServletContainerInitializer的实现放在jar包的META-INF/services目录下,有一个名为javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名。
  3. 还可以使用@HandlesTypes,在应用启动的时候加载我们感兴趣的类。
规则:
  1. 启动Tomcat
  2. 找org/springframework/spring-web/5.2.6RELEASE.jar/META-INF/services/javax.servlet.ServletContainerInitializer文件
    此文件的内容:org.springframework.web.SpringServletContainerInitializer
  3. SpringServletContainerInitializer将@HandlesTypes(WebApplicationInitializer.class)标注的所有这个类型的类都传入到onStartup方法的Set<Class<?>>里,为这些WebApplicationInitializer类型的类创建实例。
  4. 每一个WebApplicationInitializer都调用自己的onStartup。
  5. 相当于我们的SpringBootServletInitializer的类会被创建对象,并执行onStartup方法。
    在这里插入图片描述
  6. SpringBootServletInitializer实例在执行onStartup的时候会createRootApplicationContext创建容器。
    	protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
    		//1.创建SpringApplicationBuilder
    		SpringApplicationBuilder builder = createSpringApplicationBuilder();
    		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(AnnotationConfigServletWebServerApplicationContext.class);
    
    		//调用configure方法,子类重写了这个方法
    		builder = configure(builder);
    		//使用builder创建一个Spring应用
    		builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
    		SpringApplication application = builder.build();
    		if (application.getAllSources().isEmpty()
    				&& MergedAnnotations.from(getClass(), SearchStrategy.TYPE_HIERARCHY).isPresent(Configuration.class)) {
    			application.addPrimarySources(Collections.singleton(getClass()));
    		}
    		Assert.state(!application.getAllSources().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.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
    		}
    		application.setRegisterShutdownHook(false);
    		
    		//启动Spring应用
    		return run(application);
    	}
    
  7. Spring的应用就启动并且创建IOC容器。
    	public ConfigurableApplicationContext run(String... args) {
    		StopWatch stopWatch = new StopWatch();
    		stopWatch.start();
    		ConfigurableApplicationContext context = null;
    		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    		configureHeadlessProperty();
    		SpringApplicationRunListeners listeners = getRunListeners(args);
    		listeners.starting();
    		try {
    			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
    			configureIgnoreBeanInfo(environment);
    			Banner printedBanner = printBanner(environment);
    			context = createApplicationContext();
    			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
    					new Class[] { ConfigurableApplicationContext.class }, context);
    			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
    			refreshContext(context);
    			afterRefresh(context, applicationArguments);
    			stopWatch.stop();
    			if (this.logStartupInfo) {
    				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
    			}
    			listeners.started(context);
    			callRunners(context, applicationArguments);
    		}
    		catch (Throwable ex) {
    			handleRunFailure(context, ex, exceptionReporters, listeners);
    			throw new IllegalStateException(ex);
    		}
    
    		try {
    			listeners.running(context);
    		}
    		catch (Throwable ex) {
    			handleRunFailure(context, ex, exceptionReporters, null);
    			throw new IllegalStateException(ex);
    		}
    		return context;
    	}
    

启动servlet容器,在启动SpringBoot应用。

下一篇笔记:SpringBoot入门篇学习笔记(三、Docker,数据访问,启动流程和自定义starter)

学习视频(p21-p52):https://www.bilibili.com/video/BV1gW411W76m?p=21

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值