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的步骤:
- 将系统中的其他日志框架先排除出去
- 用中间包来替换原有的日志框架
- 导入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. 日志的关系
- 默认配置
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=
日志输出格式:
- 指定配置
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 {...}
-
所有访问/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 -
/**访问当前项目的任何资源,会到下面5个目录(静态资源目录)找资源:
classpath:/ 为resources目录下 "classpath:/META-INF/resources/" 优先级最高 "classpath:/resources/" "classpath:/static/" "classpath:/public/" "/" 当前项目的根目录 优先级最低
如果在resources/static下有a.html页面,访问的路径:http://localhost:8080/a.html
-
欢迎页,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
-
页面图标
在静态资源下放置名为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默认的错误处理机制
默认效果:
-
如果是用浏览器进行访问的,SpringBoot会返回一个默认的错误页面
浏览器发送请求的请求头: -
如果是其他客户端,默认响应一个json数据的错误信息
发送请求的请求头:
原理:
参照ErrorMvcAutoConfiguration(错误处理的自动配置)
给容器中添加了以下组件:
-
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; }
-
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); } }
-
ErrorPageCustomizer
@Value("${error.path:/error}") private String path = "/error"; //系统出现错误以后来到error请求进行处理(类似于web.xml注册的错误页面规则)
-
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. 定制错误页面
-
有模板引擎的情况下:error/状态码页面(templates/error)
将错误页面命名为错误状态码.html放在模板引擎目录里的error目录下,发生错误就会找此目录下找相对应的错误页面。
我们也可以用4xx和5xx页面作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找相对应的错误页面,没有才找xx页面)
页面能获取的信息:
- timestamp:时间戳
- status:状态码
- error:错误提示
- exception:异常对象
- message:异常消息
- errors:JSR303数据校验的所有错误
- 没有模板引擎的情况下:模板引擎找不到这个错误页面,就会在静态资源目录下找(templates目录下没有error),但是在静态资源目录下的页面就不能使用模板引擎语法。
- 以上都没有错误页面,使用默认的错误提示页面。
2. 定制错误的json数据
-
自定义异常处理和返回定制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; }
-
转发到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"; }
-
将我们定制的数据携带出去(第二点的扩展,重点)
出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes(是AbstractErrorController(ErrorController)规定的方法)得到的,而注册BasicErrorController时,有@ConditionalOnMissingBean(value = {ErrorController.class},search = SearchStrategy.CURRENT)注解(即如果有ErrorController或其子类,就不注册BasicErrorController)。- 我们可以完全编写一个ErrorController的实现类(或者编写AbstractErrorController的子类),重写需要的方法,放到容器中来代替BasicErrorController。
- 页面上能用的数据,或者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的默认配置:
- 自动注册了
ContentNegotiatingViewResolver
和BeanNameViewResolver
组件
自动配置了ViewResolver(视图解析器:根据方法的返回值得到视图对象(View),视图对象决定如何渲染(转发/重定向/…))
ContentNegotiatingViewResolver:组合了所有的视图解析器。
我们可以往容器添加一个自己配置的视图解析器(实现了ViewResolver的类),SpringBoot会自动将其组合进来。 - 自动注册了
Converter
、GenericConverter
、Formatter
组件
Converter:转换器(类型转换)
Formatter:格式化器(例:字符串转为日期类型(日期格式化的规则))
我们添加格式化器和转换器,只需要放到容器中即可使用。 - 自动注册了
HttpMessageConverters
组件
HttpMessageConverters:SpringMVC用来转换Http请求和响应的(User对象–>JSON
)
HttpMessageConverters:是从容器中确定的,获取所有的HttpMessageConverter。
给容器中添加HttpMessageConverter,即可使用。 - 自动注册了
MessageCodesResolver
组件。
定义错误代码生成规则。 - 自动注册了
ConfigurableWebBindingInitializer
组件
初始化WebDataBinder(web数据绑定器)(请求数据–>JavaBean)
我们可以配置一个ConfigurableWebBindingInitializer来替换默认的,只需要添加到容器中。 - 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");
}
}
原理:
- WebMvcAutoConfiguration是SpringMVC的自动配置类
- 在做其他自动配置时会导入@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
其父类的setConfigurers方法:@Configuration(proxyBeanMethods = false) public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {...}
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); //从容器中获取所有的WebMvcConfigurer @Autowired(required = false) public void setConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { this.configurers.addWebMvcConfigurers(configurers); } }
- 容器中的所有WebMvcConfigurer都会一起起作用
- 我们的配置类也会被调用
效果:SpringMVC 的自动配置和我们的扩展配置都会起作用。
3. 全面接管SpringMVC
SpringBoot对SpringMVC的自动配置不需要了,所有SpringMVC的自动配置都失效了,需要我们自己配置。
在配置类上添加@EnableWebMvc即可。
为什么加了@EnableWebMvc自动配置就全部失效了?
原理:
- @EnableWebMvc的核心
@Import({DelegatingWebMvcConfiguration.class}) public @interface EnableWebMvc {}
- DelegatingWebMvcConfiguration.class
@Configuration(proxyBeanMethods = false) public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {...}
- 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 {
- @EnableWebMvc将WebMvcConfigurationSupport组件添加到容器中。
- 添加的WebMvcConfigurationSupport组件只是SpringMVC最基本的功能。
5. 如何修改SpringBoot的默认配置
模式:
- SpringBoot在自动配置很多组件的时候,会先查看容器中是否有用户自己配置的组件(@Bean/@Component/…),如果有,就使用用户配置的组件,如果没有,才自动配置。如果有些组件可以有多个(如ViewResolver),SpringBoot会将用户配置的和默认配置的组合起来。
- 在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置。
- 在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置。
6. 配置嵌入式Servlet容器
SpringBoot默认使用Tomcat作为嵌入的Servlet容器。
定制和修改Servlet容器相关的配置
- 修改和server有关的配置(ServerProperties)
#通用的servlet容器设置 server.xxx server.port=8081 server.servlet.context-path=/crud #tomcat的设置 server.tomcat.xxx server.tomcat.uri-encoding=utf-8
- 在@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容器启动原理
- SpringBoot应用启动运行run方法
-
refreshContext(context);
SpringBoot刷新IOC容器(创建IOC容器对象并初始化容器,创建容器中的每一个组件(如果是servlet就创建AnnotationConfigServletWebServerApplicationContext,否则如果是reactive就创建AnnotationConfigReactiveWebServerApplicationContext,如果都不是创建AnnotationConfigApplicationContext)) -
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();
}
}
}
-
onRefresh();
servlet的IOC容器重写了onRefresh()方法 - servletIOC容器会创建Web服务器
createWebServer();
- 获取ServletWeb服务器工厂
ServletWebServerFactory factory = getWebServerFactory();
从IOC容器中获取ServletWebServerFactory.class组件(即TomcatServletWebServerFactory)String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
- 使用容器工厂获取web服务器
this.webServer = factory.getWebServer(getSelfInitializer());
- 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容器。
规则:
- 服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面ServletContainerInitializer实例。
- ServletContainerInitializer的实现放在jar包的META-INF/services目录下,有一个名为javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名。
- 还可以使用@HandlesTypes,在应用启动的时候加载我们感兴趣的类。
规则:
- 启动Tomcat
- 找org/springframework/spring-web/5.2.6RELEASE.jar/META-INF/services/javax.servlet.ServletContainerInitializer文件
此文件的内容:org.springframework.web.SpringServletContainerInitializer
- SpringServletContainerInitializer将@HandlesTypes(WebApplicationInitializer.class)标注的所有这个类型的类都传入到onStartup方法的Set<Class<?>>里,为这些WebApplicationInitializer类型的类创建实例。
- 每一个WebApplicationInitializer都调用自己的onStartup。
- 相当于我们的SpringBootServletInitializer的类会被创建对象,并执行onStartup方法。
- 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); }
- 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应用。
学习视频(p21-p52):https://www.bilibili.com/video/BV1gW411W76m?p=21
上一篇: 向Word文档中添加Excel 图表并与Excel数据同步更新
下一篇: 因为凤凰欲火焚身!
推荐阅读
-
移动开发基础-学习笔记二-字体图标、less、bootstrap入门
-
学习 Laravel - Web 开发实战入门笔记(1)
-
C#Web应用程序入门经典学习笔记之二
-
【技术君啃书之旅】web安全之机器学习入门 第二章笔记
-
mysql学习笔记第二节 日志系统 redolog与binlog
-
SpringBoot学习笔记【二】-[SpringMvc+SpringTask+JPA集成开发]
-
SpringBoot学习笔记【二】-[SpringMvc+SpringTask+JPA集成开发]
-
spring学习笔记(十九)springboot_web开发
-
SpringBoot入门学习笔记十(SpringBoot开发Web项目)
-
spring学习笔记(十七)springboot_web开发