[笔记迁移][Spring Boot]Web开发[4]
1. 使用Spring Boot(三步走)
- 创建Spring Boot应用,选中需要的模块
- Spring Boot默认将这些场景自动配置(xxxAutoConfiguration注入+xxxProperties映射封装),只需要在配置文件中指定少量配置就可以运行
- 编写业务逻辑代码
2. RESTful-CRUD实例
2.1 准备
2.1.1 使用Spring Initializer快速创建自带web模块的Spring Boot项目
2.1.2 Spring Boot对静态资源的映射规则
//用来设置和静态资源有关的参数,如缓存时间等
@ConfigurationProperties(prefix = "spring.resources",ignoreUnknownFields = false)
public class ResourceProperties implements ResourceLoaderAware, InitializingBean
//org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration
//静态资源处理器
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
Integer cachePeriod = this.resourceProperties.getCachePeriod();
if (!registry.hasMappingForPattern("/webjars/**")) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(cachePeriod));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern(); //this.staticPathPattern = "/**"
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(this.resourceProperties.getStaticLocations()).setCachePeriod(cachePeriod)); //staicLocation->ResourceProperty#RESOURCE_LOCATIONS,是一个常量数组
}
}
}
//配置欢迎页映射
@Bean
public WebMvcAutoConfiguration.WelcomePageHandlerMapping welcomePageHandlerMapping(ResourceProperties resourceProperties) {
return new WebMvcAutoConfiguration.WelcomePageHandlerMapping(resourceProperties.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
}
/*
private String[] getStaticWelcomePageLocations() {
String[] result = new String[this.staticLocations.length];
for(int i = 0; i < result.length; ++i) {
String location = this.staticLocations[i];
if (!location.endsWith("/")) {
location = location + "/";
}
result[i] = location + "index.html";
}
*/
//配置图标
@Configuration
@ConditionalOnProperty(value = {"spring.mvc.favicon.enabled"},matchIfMissing = true)
public static class FaviconConfiguration {
private final ResourceProperties resourceProperties;
public FaviconConfiguration(ResourceProperties resourceProperties) {
this.resourceProperties = resourceProperties;
}
@Bean
public SimpleUrlHandlerMapping faviconHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(-2147483647);
//将所有**/favicon.ico映射给faviconRequestHandler
mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
this.faviconRequestHandler()));
return mapping;
}
/*
List<Resource> getFaviconLocations() {
List<Resource> locations = new ArrayList(this.staticLocations.length + 1);
if (this.resourceLoader != null) {
String[] var2 = this.staticLocations;
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
String location = var2[var4];
locations.add(this.resourceLoader.getResource(location));
}
}
*/
-
所有匹配/webjars/**的资源请求都去 classpath:/META-INF/resources/webjars/ 找资源
【webjars】:以jar包的方式引入静态资源:https://www.webjars.org/ ,可以查询到所需资源的对应版本并将dependency引入pom.xml,如JQuery<dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.2.1</version> </dependency>
访问路径举例:localhost:8080/webjars/jquery/3.2.1/jquery.js -
/** 访问当前项目的任何资源,如果没有处理,将从以下静态资源文件夹中查找
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/
/即当前项目的根路径
访问路径举例:localhost:8080/assets/js/Chart.min.js -
欢迎页:静态资源文件夹下的所有index.html页面;被/**映射
访问路径举例:localhost:8080/ => index.html -
所有的**/favicon.ico都是在静态资源文件夹下找
-
通过在主配置文件中修改spring.resources.static-locations可以自定义静态资源文件夹
2.2 Spring Boot默认不支持JSP => 推荐模板引擎Thymeleaf
2.2.1 引入Thymeleaf-starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--默认引用2.1.6,过低,需要切换3.x,在抽取的<properties>中加入如下配置已覆盖父依赖-->
<thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
<!--布局功能的支持程序,thymeleaf3主程序,layout2以上版本-->
<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
2.2.2 使用
只要把html页面置于classpath:/templates/中,thymeleaf便能自动渲染
在h5页面的<html>中导入thymeleaf的命名空间以获取语法提示
<html lang="en" xmlns:th="http://www.thymeleaf.org">
//默认配置映射ThymeleafProperties
@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");
//只要把HTML页面置于classpath:/templates/中,thymeleaf便能自动渲染
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 = "HTML5";
private Charset encoding;
private MimeType contentType;
private boolean cache;
private Integer templateResolverOrder;
private String[] viewNames;
private String[] excludedViewNames;
private boolean enabled;
}
2.2.3 语法
-
th:text 替换当前元素内的文本内容
=>“th:a属性” 可以对html原生a属性进行替换(渲染后,直接访问静态页面不会发生替换) -
表达式
Simple Expression 简单表达式 Variable Expression 变量表达式 ${} 底层是OGNL表达式,OGNL功能
内置Basic Object:
#ctx: the context object
#vars: the context variables
#locale: the context locale
#request: HttpServletRequest
#response: HttpServeltResponse
#session: HttpSession
#servletContext: ServletContext
内置工具对象:
#execInfo: information about the template being processed.
#message: methods for obtaining externalized messages inside variables expression, in the same way as they would be obtained using #{…}
#uris: methods for escaping parts of URLs/URIs
#conversions: methods for executingthe configured conversion service(if any)
#dates: methods for java.util.Date objects: formatting, component extraction…
#calendars: analogous to #date, but for java.util.Calendar objects
#numbers: methods for formatting numeric objects
#strings: methods for String objects: contains, startWith, prepending/appending…
#objects: methods for objects in general
#bools: method for boolean evaluation
#arrays: method for arrays
#lists
#sets
#maps
#aggregates: methods for creating aggregates on array of collection
#ids: methods for dealing with id attributes that might be repeatedSelecting Variable Expression 选择表达式 在${}功能的基础上,补充了功能:配合th:object进行使用,在th:object指定对象后,可以直接在子模块使用*.{字段名}取值 Message Expression 国际化表达式 #{},获取国际化内容 Link URL Expression @{},定义URL
@{/order/process(execId=${execId},execType=‘FAST’)}Fragment Expression 文档片段表达式 -{…} 字面量表达式,数学表达式,布尔表达式,运算符表达式等与OGNL一致
【Special tokes 特殊表达式(无操作):_】 -
常见场景实例
<body>
success!
<!-- th:text 将div内的文本内容设置为指定值-->
<div th:text="${hello}"></div>
<div th:utext="${hello}"></div>
<hr/>
<!--与增强foreach相似的遍历th:each, 每次遍历都会生成当前标签-->
<h4 th:text="${user}" th:each="user:${users}"></h4>
<hr>
<h4>
<!--[[]]与th:text等价,[()]与th:utext等价 -->
<span th:each="user:${users}">[[${user}]]</span>
</h4>
</body>
2.3 底层 SpringMVC 自动配置
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.
- Support for serving static resources, including support for WebJars (see below).
- Automatic registration of Converter, GenericConverter, Formatter beans.
- Support for HttpMessageConverters (see below).
- Automatic registration of MessageCodesResolver (see below).
定义错误代码生成规则- Static index.html support.
- Custom Favicon support (see below).
- Automatic use of a ConfigurableWebBindingInitializer bean (see below).
初始化WebDataBinder
- 自动配置视图解析器ViewResolver
//1.1 ContentNegotiatingViewResolver:组合所有的视图解析器 @Bean @ConditionalOnBean({ViewResolver.class}) @ConditionalOnMissingBean(name = {"viewResolver"},value = {ContentNegotiatingViewResolver.class}) public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) { ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver(); resolver.setContentNegotiationManager((ContentNegotiationManager)beanFactory .getBean(ContentNegotiationManager.class)); resolver.setOrder(-2147483648); return resolver; } //1.1.1 ContentNegotiatingViewResolver解析最佳视图实现 public View resolveViewName(String viewName, Locale locale) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes"); List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest()); if (requestedMediaTypes != null) { List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes); /* 底层getCadidateViews()的核心代码 Iterator var5 = this.viewResolvers.iterator(); while(var5.hasNext()) { ViewResolver viewResolver = (ViewResolver)var5.next(); View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { candidateViews.add(view); }*/ View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs); if (bestView != null) { return bestView; } } //1.1.2 ContentNegotiatingViewResolver获取视图解析器 //如何自定义视图解析器?给容器注入自定义ViewResolverBean,ContentanargotiatingViewResolver自动组合进来 protected void initServletContext(ServletContext servletContext) { Collection<ViewResolver> matchingBeans = BeanFactoryUtils .beansOfTypeIncludingAncestors(this.getApplicationContext(), ViewResolver.class) .values(); //... }
- 自动配置类型转换器Converter与格式化器Formatter ===> ConverterFormatter
//2.1 DateFormatter @Bean @ConditionalOnProperty(prefix = "spring.mvc",name = {"date-format"})//需要在配置文件中指定日期格式化规则 public Formatter<Date> dateFormatter() { return new DateFormatter(this.mvcProperties.getDateFormat()); } //2.2 HttpMessageConverters从容器中获取(内部类,只有一个有参构造器,所有的值都从容器中取出) //如何自定义HttpMessageConverter?给容器注入自定义HttpMessageConverterBean public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, @Lazy HttpMessageConverters messageConverters, ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider); //2.3 添加格式化器 //如何自定义格式化转换器?给容器注入自定义ConverterBean public void addFormatters(FormatterRegistry registry) { Iterator var2 = this.getBeansOfType(Converter.class).iterator(); while(var2.hasNext()) { Converter<?, ?> converter = (Converter)var2.next(); registry.addConverter(converter); } var2 = this.getBeansOfType(GenericConverter.class).iterator(); while(var2.hasNext()) { GenericConverter converter = (GenericConverter)var2.next(); registry.addConverter(converter); } var2 = this.getBeansOfType(Formatter.class).iterator(); while(var2.hasNext()) { Formatter<?> formatter = (Formatter)var2.next(); registry.addFormatter(formatter); } }
- 自动初始化数据绑定器WebDataBinder
//如何自定义绑定器?给容器注入自定义ConfigurableWebDataBindingInitializer protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() { try { return (ConfigurableWebBindingInitializer)this.beanFactory.getBean(ConfigurableWebBindingInitializer.class); } catch (NoSuchBeanDefinitionException var2) { return super.getConfigurableWebBindingInitializer(); } } //public void initBinder(WebDataBinder binder, WebRequest request);
2.4 扩展SpringMVC
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.
※编写一个配置类@Configuration,集成WebMvcConfigurerAdapter且不标注@EnableWebMvc :既保留了所有的自动配置,也能使用自定义扩展配置
//使用WebMvcCofigurerAdapter来扩展SpringMVC的功能
//WebMvcConfigurerAdapter
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//super.addInterceptors(registry);
registry.addInterceptor(new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}).addPathPatterns("/hello");
}
}
【原理】
(1)WebMvcAutoConfiguration是SpringMVC的自动配置类
(2)其中,同样使用WebMvcConfigurerAdapter进行组件注入
@Configuration
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter
(3)在做其他自动配置时会导入:@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class}),使WebMvcConfigurer(WebMvcConfigurerAdapter实现的接口)中所有的配置起作用,即包括用户自定义的扩展配置类
@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration
//DelegatingWebMvcConfiguration
//从容器中获取所有WebMvcConfigurer并注入
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
/*一个参考调用实现,将所有的WebMvConfigurer相关配置都来一起调用
public void addViewControllers(ViewControllerRegistry registry) {
Iterator var2 = this.delegates.iterator();
while(var2.hasNext()) {
WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
delegate.addViewControllers(registry);
}
}*/
2.5 全面接管SpringMVC
在配置类上标注@EnableWebMvc,舍弃Spring Boot对SpringMVC的所有自动配置,所有SpringMVC配置都需要自己配置
【原理】
(1)EnableWebMvc核心
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc
(2)DelegatingWebMvcConfiguration本质
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
(3)WebMvcAutoConfiguration启用的条件
//只有容器中没有WebMvcConfigurationSupport时,该自动配置类才生效,也就是说一旦某配置类@EnableWebMvc之后,自动配置类失效
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration
(4)自动导入的WebMvcConfigurationSupport(DelegatingWebMvcConfiguration)只有基本设置,相当于之前<mvc:annotation-driven/>
2.6 RESTful CRUD实验
2.6.1 访问首页
方法一:访问URL,Controller返回"index"跳至模板引擎渲染目录template下的index.html渲染
方法二:在自定义配置类(extends WebMvcConfigurerAdapter)中实现addControllerView
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
registry.addViewController("/index.html").setViewName("login");
}
//或 在当前配置类中向容器再注入另一个WebMvcConfigurerAdapter至IOC容器,以使WebMvcAutoConfiguration自动配置叠加使用
@Bean
public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){
return new WebMvcConfigurerAdapter() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
registry.addViewController("/index.html").setViewName("login");
}
};
}
2.6.2 修改资源引用(webjars)
<!--1. 导入对应webjars的依赖-->
<!--2. 导入thymeleaf提示命名空间-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--3. 使用th:href配合@{}进行资源覆盖,类似${pageContext.request.contextPath}-->
<!-- Bootstrap core CSS -->
<link href="asserts/css/bootstrap.min.css" th:href="@{/webjars/bootstrap/4.0.0/css/bootstrap.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="asserts/css/signin.css" th:href="@{asserts/css/signin.css}" rel="stylesheet">
2.6.3 国际化
-
原生SpringMVC使用步骤
编写国际化配置文件properties
向IOC容器注册ResourceBundleMessageSource,以管理国际化资源文件
在页面使用fmt:message取出当前Locale对应的内容
使用_locale指定切换目标 -
Spring Boot中使用
(1)编写国际化配置文件,抽取页面需要显示地国际化消息
Tip:IDEA检测国际化配置文件properties,按照基名basename进行分组,而且在具体Locale对应的配置文件中可以使用ResourceBuddle视图进行批量添加与更改#login.properties,默认配置 login.btn=登录~ login.password=密码~ login.remember=记住我~ login.tip 请登录~ login.username=用户名~ #login_zh_CN.properties login.btn=登录 login.password=密码 login.remember=记住我 login.tip=请登录 login.username=用户名 #login.en_US.properties login.btn=Sign in login.password=Password login.remember=RememberMe login.tip=Please sign in login.username=UserName
(2)Spring Boot自动配置好国际化资源文件的组件,开发人员只要在全局配置文件中指定基名
spring.messages.basename=i18n.login
【原理】
@ConfigurationProperties(prefix = "spring.messages") public class MessageSourceAutoConfiguration { //... private String basename = "messages"; //基名默认值是messages,可以直接放在类路径下叫message.properties //... @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); if (StringUtils.hasText(this.basename)) { //设置国际化资源文件的基名 messageSource.setBasenames(StringUtils. commaDelimitedListToStringArray(StringUtils. trimAllWhitespace(this.basename))); } if (this.encoding != null) { messageSource.setDefaultEncoding(this.encoding.name()); } messageSource.setFallbackToSystemLocale(this.fallbackToSystemLocale); messageSource.setCacheSeconds(this.cacheSeconds); messageSource.setAlwaysUseMessageFormat(this.alwaysUseMessageFormat); return messageSource; } }
(3)到页面获取国际化的值:使用th:对应属性以替换
<img class="mb-4" src="asserts/img/bootstrap-solid.svg" th:src="@{asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72"> <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1> <label class="sr-only" th:text="#{login.username}">Username</label> <input type="text" class="form-control" th:placeholder="#{login.username}" placeholder="Username" required="" autofocus=""> <label class="sr-only" th:text="#{login.password}">Password</label> <input type="password" class="form-control" th:placeholder="#{login.password}" placeholder="Password" required=""> <!-- 行内表达式 --> <input type="checkbox" value="remember-me"> [[#{login.remember}]] <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
(4)语言切换
原生SpringMVC原理:国际化依赖Locale -> LocaleResolver
<1> Spring Boot自动配置LocaleResolver:默认情况根据请求头中的区域信息获取Locale进行国际化@Bean @ConditionalOnMissingBean @ConditionalOnProperty(prefix = "spring.mvc",name = {"locale"}) public LocaleResolver localeResolver() { if (this.mvcProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.WebMvcProperties.LocaleResolver.FIXED) { return new FixedLocaleResolver(this.mvcProperties.getLocale()); } else { AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); localeResolver.setDefaultLocale(this.mvcProperties.getLocale()); return localeResolver; } }
<2>自定义LocaleResolver并注入容器替换默认行为以实现点击切换国际化需求
<!-- 在前台以点击链接的方式切换Locale,在thymeleaf中,传递参数使用() --> <a class="btn btn-sm" th:href="@{/index.html(locale='zh_CN')}">中文</a> <a class="btn btn-sm" th:href="@{/index.html(locale='en_US')}">English</a>
//自定义组件LocaleResolver public class MyLocaleResolver implements LocaleResolver{ @Override public Locale resolveLocale(HttpServletRequest httpServletRequest) { String localeStr = httpServletRequest.getParameter("locale"); //使用thymeleaf的StringUtils来判断参数值是否为空,若为空则使用默认 Locale locale=Locale.getDefault(); if(!StringUtils.isEmpty(localeStr)){ //分割得到语言和国家代码并使用Locale构造器创建Locale String[] split = localeStr.split("_"); locale=new Locale(split[0],split[1]); System.out.println(split[0]+"_"+split[1]); } return locale; } @Override public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) { } } //在配置类中向IOC容器注入 @Bean public LocaleResolver localeResolver(){ return new MyLocaleResolver(); }
2.6.4 登录与拦截器
-
实现登录基本要点:
(1)为体现RESTful风格,映射注解可以使用@GetMapping、@PostMapping、@PutMapping、@Deletemapping以取代之前对应的@RequestMapping
(2)由于模板引擎的缓存功能,当修改静态页面时,不会立即更新,需要在主配置文件中禁用spring.thymeleaf.cache=false
(3)IDEA Tips:在运行期间,当静态页面发生改变时,并不会立即编译刷新。在禁用缓存的前提下,使用 ctrl+F9 强制编译刷新
(4)登录消息的显示(优先级与工具类的使用)<!-- 判断,th:if的优先级是3,当判断为true时,从4开始的优先级才会被执行或显示 --> <p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"/>
(5)防止重复提交最好使用重定向
(6)注意:想要被引擎动态渲染,必须解析请求映射至某模板视图(xxx.html->xxx->/templates/xxx.html),否则将在直接在静态资源文件夹下查找返回未渲染模板 -
登录检查拦截器要点:
(1)实现自定义Interceptor//登录检查 public class LoginHandlerInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { HttpSession session = httpServletRequest.getSession(); Object loginUser = session.getAttribute("loginUser"); if(loginUser == null){ //未登录,返回登录页面 httpServletRequest.setAttribute("msg","没有操作全限,请先登录"); httpServletRequest.getRequestDispatcher("/index.html").forward(httpServletRequest,httpServletResponse); }else{ //已经登录,放行 return true; } return false; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
(2)向自定义配置类中添加该拦截器
Spring Boot已经做好静态资源映射,不会被拦截器拦截/* 使用addPathPatterns设置当前拦截器所能拦截的URL。/**即Ant表示所有层级 使用excludePathPatterns设置需要排除的请求,可变参数列表。显然,登录页与登录请求需要排除 */ @Override public void addInterceptors(InterceptorRegistry registry) { //Spring Boot 已经做好静态资源映射,不会被拦截器拦截 registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**") .excludePathPatterns("/index.html","/","/user/login"); }
2.6.5 CRUD与原生SpringMVC的Controller操作一致,只记录前台thymeleaf的使用
- thymeleaf 公共页面元素抽取与引入
<!--1. 抽取公共片段--> <div th:fragment="copy"> © 2011 The Good Thymes Virtual Grocery </div> <!-- 2.引入公共片段 --> <div th:insert="~{footer::copy}"/> <!-- 两种规则: ~{templatename::selector} 模板名::选择器 ~{templatename::fragmentname} 模板名::片段名 模板名不带前后缀,thymeleaf会自动拼串解析(路径还是从templates下开始) --> <!-- 默认效果:被insert的功能片段被套入div --> <!-- 三种引入公共片段的方法将导致不一样的效果 <div th:insert="~{footer::selector}"/>,将公共片段整个插入至指定声明引入元素中 <div th:replace="~{footer::selector}"/>,声明引入的元素被替换为公共片段 <div th:include="~{footer::selector}"/>,将被引入的片段的内容包含进声明引入的元素中,拆除最外部标签 若使用属性进行引入,可以不用写~{}; 若使用行内引入,则必须写[[~{}]],[(~{})] -->
- 动态条目高亮:参数化的片段签名
<!--抽取时在需要选择的元素中指定th:class,在其中使用三元判断表达式进行变量判断。这里activeUri即为自定义参数--> th:class="${activeUri=='emps'?'nav-link active':'nav-link'}" <!--引用时传入参数键值对--> <!-- commons/bar::sidebar => 模板名::ID选择器 --> <div th:replace="commons/bar::sidebar(activeUri='main.html')"></div>
- 列表迭代与格式化
<tbody> <!-- 被th:each标记的行迭代一次创建该元素一次 --> <tr th:each="emp:${emps}"> <td th:text="${emp.id}"></td> <td>[[${emp.lastName}]]</td> <td th:text="${emp.email}"></td> <td th:text="${emp.gender==0}?'女':'男'"></td> <td th:text="${emp.department.departmentName}"></td> <td th:text="${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}"></td> <td> <a class="btn btn-sm btn-primary" href="@{/emp/}+${emp.id}">编辑</button> <a class="btn btn-sm btn-danger" href="@{/emp/}+${emp.id}+">删除</button> </td> </tr> </tbody>
- 下拉列表的时候需要对子标签<option>进行th:each迭代
<select> <option th:each="dept:${depts}" th:text="${dept.departmentName}" value="${dept.deptid}"></option> </select>
- 日期型数据格式化,在WebMvcAutoConfiguration中自动配置的DateFormatter默认使用的yyyy/MM/dd的格式,可以在配置文件中指定spring.mvc.date-format=yyyy-MM-dd改变格式化类型为yyyy-MM-dd
- 更新/删除操作时,需要传递id,但注意@{}与${}是两种表达式,需要"+"号拼接
- 回显时,text类型使用<input>属性th:value进行置值;
radio类型使用th:checked=true/false进行选择,当然需要判断th:checked=${emp.gender==1};
option类型使用th:selected=true/false进行判断,判断th:checked=${dept.id==emp.department.id} - 添加/更新两个操作使用同一个页面时,也需要使用三元表达式判断如${emp!=null}?${emp.lastName}
- HiddenHttpMethodFilter由Spring Boot自动配置,同样需要进行判断以决定是否添加隐藏域
<input type="hidden" name="_method" value="PUT" th:if="${emp!=null}"/>
- 指定属性th:attr=“key1=val1,key2=val2”,可以为标签添加自定义属性
3. Spring Boot错误处理机制
3.1 效果表现
- 浏览器的默认效果:返回一个默认错误页面
请求头: - 通过Postman查看其他设备客户端返回情况:默认返回JSON
请求头:{ "timestamp": 1537180989124, "status": 404, "error": "Not Found", "message": "No message available", "path": "/abc" }
3.2 原理
参照org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration,给容器添加了如下组件:
3.2.1 DefaultErrorAttributes
//设置Model,传递Error页面显示信息
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, requestAttributes);
this.addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
this.addPath(errorAttributes, requestAttributes);
return errorAttributes;
}
private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
Integer status = (Integer)this.getAttribute(requestAttributes, "javax.servlet.error.status_code");
if (status == null) {
errorAttributes.put("status", Integer.valueOf(999));
errorAttributes.put("error", "None");
} else {
errorAttributes.put("status", status);
try {
errorAttributes.put("error", HttpStatus.valueOf(status.intValue()).getReasonPhrase());
} catch (Exception var5) {
errorAttributes.put("error", "Http Status " + status);
}
}
}
private void addErrorDetails(Map<String, Object> errorAttributes, RequestAttributes requestAttributes, boolean includeStackTrace) { Throwable error = this.getError(requestAttributes);
if (error != null) {
while(true) {
if (!(error instanceof ServletException) || error.getCause() == null) {
errorAttributes.put("exception", error.getClass().getName());
this.addErrorMessage(errorAttributes, error);
if (includeStackTrace) {
this.addStackTrace(errorAttributes, error);
}
break;
}
error = ((ServletException)error).getCause();
}
}
Object message = this.getAttribute(requestAttributes, "javax.servlet.error.message");
if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null) && !(error instanceof BindingResult)) {
errorAttributes.put("message", StringUtils.isEmpty(message) ? "No message available" : message);
}
}
private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) {
BindingResult result = this.extractBindingResult(error);
if (result == null) {
errorAttributes.put("message", error.getMessage());
} else {
if (result.getErrorCount() > 0) {
errorAttributes.put("errors", result.getAllErrors());
errorAttributes.put("message", "Validation failed for object='" + result.getObjectName() + "'. Error count: " + result.getErrorCount());
} else {
errorAttributes.put("message", "No errors");
}
}
}
3.2.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.isIncludeStackTrace(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
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = this.getStatus(request);
return new ResponseEntity(body, status);
}
//...
}
3.2.3 ErrorPageCustomizer
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix() + this.properties.getError().getPath());
errorPageRegistry.addErrorPages(new ErrorPage[]{errorPage});
}
//getPath()
//系统出现错误以后,发送/error请求进行处理,类似原生在web.xml中的注册的异常映射
@Value("${error.path:/error}")
private String path = "/error";
3.2.4 DefaultErrorViewResolver
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = this.resolve(String.valueOf(status), 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指定的视图方法如error/404.html;否则在静态资源文件夹下找errorViewName对应的页面
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
3.2.5 流程
一旦出现4xx或5xx之类的错误,ErrorPageCustomizer就会生效(定制错误的响应规则),发送/error请求;会被BaseErrorController处理,去哪个页面是由ErroViewResolver解析决定的
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
Iterator var5 = this.errorViewResolvers.iterator();
//遍历所有的ErrorViewResolver得到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;
}
}
3.3 如何自定义错误响应
3.3.1 如何定制错误页面
情况 | 效果 |
---|---|
配置模板引擎 | 当发生错误代码为statusCode的错误出现时,查找template/error/{statusCode}.html页面,如templates/error/404.html会显示404错误; Spring Boot还提供了4xx,5xx系列通用配置,即可以创建templates/error/4xx.html处理400系的错误,如403,404,405… 优先寻找精确的${statusCode}.html 可以在页面获取的值:timestamp, status, error, exception, message, errors(JSR303校验的错误) |
没有模板引擎(模板引擎下找不到error/{statusCode}.html) | 查找静态资源文件夹,但无法获取动态信息 |
以上都没有错误页面 | 转到默认Spring Boot的错误页面error (自动配置类注入到容器中的View,使用SpEL形成的页面) |
3.3.2 如何定制JSON的错误数据
方法 | 说明 |
---|---|
使用SpringMVC原生注解 | @ControllerAdvice, @ExceptionHandler, @ResponseBody 但访问设备自适应被取消,不管是浏览器还是其他设备都会返回自定义JSON |
将当前隐藏域转发给/error即使用BasicErrorController再处理一次 | 参见方法二, 如果仅自定义信息可达到基本自适应,问题在于状态码非4xx/5xx错误,而是200成功 需要在HttpRequest中置入一个javax.servlet.error.status_code以解决该问题 但只能传递自定义Exception,自定义ControllerAdvice中定制的临时消息并没有被传递 |
将自定义ControllerAdvice中的临时消息进行返回 | 在方法二的基础上参见方法三, 出现错误后,会发/error请求,被BasicErrorController处理,响应传递出去的数据是由getErrorAttribute()得到(父类AbstractErrorController implements ErrorController): 实现ErrorController/继承AbstractErrorController,重写部分方法,注入到容器中【过于复杂】 页面上能得到的数据/JSON能返回的数据,都是通过ErrorAttributes.getErrorAttributes()得到,默认使用DefaultErrorAttributes |
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String,Object> map=new HashMap<>();
map.put("code","usernotexists");
map.put("message",e.getMessage());
request.setAttribute("javax.servlet.error.status_code",400);
return "forward:/error";
}
}
/*
底层支持:
HttpStatus status = this.getStatus(request);
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");
...
}
*/
/*
直接实现接口ErrorController或继承BasicErrorController并注入的底层支持:
@Bean
@ConditionalOnMissingBean(value = {ErrorController.class},search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
}
*/
/*
实现ErrorAttributes并注入的底层支持:
@Bean
@ConditionalOnMissingBean(value = {ErrorAttributes.class},search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
*/
//向容器中注入MyErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
//返回的Map即页面和JSON所能获取的全部键值对
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
//设置自定义的键值对
map.put("flag","0");
map.put("emmm","emmmm");
/*RequestAttributes包装了HttpServletRequest,
故能够联动ControllerAdvice,
但必须在ExceptionHandlerHandler中setAttribute()
注意:第二参数代表域的范围
*/
Map<String,Object> ext=requestAttributes.getAttribute("ext",0);
map.put("ext",ext);
return map;
}
}
4. 配置嵌入式Servlet容器(embeded)
4.1 Spring Boot默认使用Tomcat作为嵌入式Servlet容器
4.2 问题
- 如何定制和修改Servlet容器的相关配置
- Spring Boot支持其他的Servlet容器实现
4.3 解决方案
- 定制和修改Servlet容器相关配置有两个方案(同一原理):
(1)在全局配置文件中修改与server有关的配置(ServerProperties)server.port=8081 server.context-path=/crud server.tomcat.uri-encoding=UTF-8 #通用的Servlet容器设置:server.xxx=yyy #配置Tomcat的配置:server.tomcat.xxx=yyy
(2)自定义一个EmbeddedContainerCustomizer@ConfigurationProperties(prefix = "server",ignoreUnknownFields = true) public class ServerProperties implements EmbeddedServletContainerCustomizer, EnvironmentAware, Ordered{ private Integer port; private InetAddress address; private String contextPath; private String displayName = "application"; @NestedConfigurationProperty private ErrorProperties error = new ErrorProperties(); private String servletPath = "/"; private final Map<String, String> contextParameters = new HashMap(); private Boolean useForwardHeaders; private String serverHeader; private int maxHttpHeaderSize = 0; private int maxHttpPostSize = 0; private Integer connectionTimeout; private ServerProperties.Session session = new ServerProperties.Session(); @NestedConfigurationProperty private Ssl ssl; @NestedConfigurationProperty private Compression compression = new Compression(); @NestedConfigurationProperty private JspServlet jspServlet; private final ServerProperties.Tomcat tomcat = new ServerProperties.Tomcat(); private final ServerProperties.Jetty jetty = new ServerProperties.Jetty(); private final ServerProperties.Undertow undertow = new ServerProperties.Undertow(); private Environment environment; //... }
//在自定义WebMvcConfigurureAdapter中注入 @Bean public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){ return new EmbeddedServletContainerCustomizer() { //定制嵌入式的Servlet容器的相关规则 @Override public void customize(ConfigurableEmbeddedServletContainer configurableEmbeddedServletContainer) { configurableEmbeddedServletContainer.setPort(8083); } }; }
- 替换为其他Servlet容器实现
Spring Boot默认支持三类容器:Tomcat,Jetty(擅长P2P长连接),Undertow(擅长高并发但不支持JSP)
以Jetty为例,在pom.xml的spring-boot-starter中排除默认容器Tomcat依赖(引入web模块时被自动添加),然后添加spring-boot-starter-jetty依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-tomcat</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>spring-boot-starter-jetty</groupId> <artifactId>org.springframework.boot</artifactId> </dependency>
4.4 注册自定义Servlet,Filter,Listener
由于Spring Boot默认通过jar包启动嵌入式Servlet容器来启动Spring Boot的WebApplication,没有web.xml,只能在配置类中注入三大组件对应的注册器并完成配置
-
注册ServletRegistrationBean
@Bean public ServletRegistrationBean myServlet(){ //public ServletRegistrationBean(Servlet servlet, String... urlMappings) ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new MyServlet(),"/myServlet"); return servletRegistrationBean; }
-
注册FilterRegistrationBean
@Bean public FilterRegistrationBean myFilter(){ FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(new MyFilter()); filterRegistrationBean.setUrlPatterns(Arrays.asList("/hello","/myServlet")); return filterRegistrationBean; }
-
注册ServletListenerRegistrationBean
public ServletListenerRegistrationBean myListener(){ ServletListenerRegistrationBean<MyListener> servletListenerRegistrationBean = new FilterRegistrationBean<>(new MyListener()); return servletListenerRegistrationBean; }
-
Spring Boot自动注册SpringMVC DispatcherServlet
//DispatcherServletAutoConfiguration @Bean(name = {"dispatcherServletRegistration"}) @ConditionalOnBean(value = {DispatcherServlet.class},name = {"dispatcherServlet"}) public ServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet) { ServletRegistrationBean registration = new ServletRegistrationBean(dispatcherServlet, new String[]{this.serverProperties.getServletMapping()}); //private String servletPath = "/"; //默认拦截"/"即所有请求:包含静态资源请求,但不拦截JSP请求 //配置映射ServerProperties,可以在配置文件中指定server.servletPath来修改SpringMVC的前端控制器拦截的请求路径 registration.setName("dispatcherServlet"); registration.setLoadOnStartup(this.webMvcProperties.getServlet().getLoadOnStartup()); if (this.multipartConfig != null) { registration.setMultipartConfig(this.multipartConfig); } return registration; } }
4.5 嵌入式Servlet容器自动配置原理
- EmbededServletContainerAutoConfiguration
@AutoConfigureOrder(-2147483648) @Configuration @ConditionalOnWebApplication @Import({EmbeddedServletContainerAutoConfiguration.BeanPostProcessorsRegistrar.class}) public class EmbeddedServletContainerAutoConfiguration{ @Configuration @ConditionalOnClass({Servlet.class, Tomcat.class}) //判断当前是否引出了Tomcat依赖 @ConditionalOnMissingBean(value = {EmbeddedServletContainerFactory.class},search = SearchStrategy.CURRENT)//判断当前容器中没有自定义EmbededServletContainerFactory来创建嵌入式Servlet容器 public static class EmbeddedTomcat { public EmbeddedTomcat() {} @Bean public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() { return new TomcatEmbeddedServletContainerFactory(); } } }
- EmbeddedServletContainerFactory
public interface EmbeddedServletContainerFactory { EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... var1); }
以TomcatEmbeddedServletContainerFactory为例public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) { //创建一个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); 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,返回TomcatEmbeddedServletContainer,并且启动Tomcat启动 return this.getTomcatEmbeddedServletContainer(tomcat); }
- EmbededServletContainer
以TomcatEmbeddedServletContainer为例public TomcatEmbeddedServletContainer(Tomcat tomcat, boolean autoStart) { this.monitor = new Object(); this.serviceConnectors = new HashMap(); Assert.notNull(tomcat, "Tomcat Server must not be null"); this.tomcat = tomcat; this.autoStart = autoStart; this.initialize(); } private void initialize() throws EmbeddedServletContainerException { logger.info("Tomcat initialized with port(s): " + this.getPortsDescription(false)); Object var1 = this.monitor; synchronized(this.monitor) { try { this.addInstanceIdToEngineName(); try { final Context context = this.findContext(); context.addLifecycleListener(new LifecycleListener() { public void lifecycleEvent(LifecycleEvent event) { if (context.equals(event.getSource()) && "start".equals(event.getType())) { TomcatEmbeddedServletContainer.this.removeServiceConnectors(); } } }); //启动 this.tomcat.start(); this.rethrowDeferredStartupExceptions(); try { ContextBindings.bindClassLoader(context, this.getNamingToken(context), this.getClass().getClassLoader()); } catch (NamingException var5) { ; } this.startDaemonAwaitThread(); } catch (Exception var6) { containerCounter.decrementAndGet(); throw var6; } } catch (Exception var7) { this.stopSilently(); throw new EmbeddedServletContainerException("Unable to start embedded Tomcat", var7); } } }
- EmbeddedServletContainerCustomerizer 修改Servlet容器配置原理
//BeanPostProcessorsRegistrar,给容器导入一些组件,最重要的是导入EmbeddedServletContainerCustomizerBeanPostProcessor //后置处理器:在Bean初始化前后(构造器调用完成后,setter还没有置值前)执行参数初始化 public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { if (this.beanFactory != null) { this.registerSyntheticBeanIfMissing(registry, "embeddedServletContainerCustomizerBeanPostProcessor", EmbeddedServletContainerCustomizerBeanPostProcessor.class); this.registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor", ErrorPageRegistrarBeanPostProcessor.class); } } //EmbeddedServletContainerCustomizerBeanPostProcessor中的重要方法 public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { //如果当前初始化的是一个ConfigurableEmbeddedServletContainer if (bean instanceof ConfigurableEmbeddedServletContainer) { this.postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer)bean); } return bean; } private void postProcessBeforeInitialization(ConfigurableEmbeddedServletContainer bean) { Iterator var2 = this.getCustomizers().iterator(); //获取所有的定制器,调用每一个定制器的customize方法来给Servlet容器设置属性 while(var2.hasNext()) { EmbeddedServletContainerCustomizer customizer = (EmbeddedServletContainerCustomizer)var2.next(); customizer.customize(bean); } } private Collection<EmbeddedServletContainerCustomizer> getCustomizers() { if (this.customizers == null) { this.customizers = new ArrayList( //核心:从IOC容器中获取所有EmbeddedServletContainerCustomizer定制Servlet容器属性 this.beanFactory.getBeansOfType(EmbeddedServletContainerCustomizer.class, false, false).values()); Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE); this.customizers = Collections.unmodifiableList(this.customizers); } return this.customizers; }
-
整个流程:
Spring Boot根据导入的依赖情况给容器中添加响应的内嵌EmbededServletContainerFactory
容器中某个组件被创建就会触发后置处理器BeanPostProcessorsRegistrar,判断是否为ConfigurableEmbeddedServletContainer(EmbeddedServletContainerFactory的父接口)
BeanPostProcessorRegistrar从IoC容器中获取所有的EmbeddedServletContainerCustomizer,调用定制其的customize()
4.6 嵌入式Servlet容器启动原理
- Spring Boot应用启动运行run(),刷新IoC容器refreshContext(context);
创建IoC容器并初始化。如果时WebApplication创建AnnotationConfigEmbeddedWebApplicationContext,否则创建AnnotationConfigApplicationContext; - refresh(context); 刷新刚才创建好的IoC容器
onRefresh()有WebIOC容器重写,调用createEmbeddedServletContainer()
创建的第一步,从容器中获取嵌入式Servlet容器工厂EmbeddedServletContainerFactoryprotected void onRefresh() { super.onRefresh(); try { this.createEmbeddedServletContainer(); } catch (Throwable var2) { throw new ApplicationContextException("Unable to start embedded container", var2); } }
TomcatEmbeddedServletContainerFactory被创建,后置处理器被触发,Servlet容器被配置EmbeddedServletContainerFactory containerFactory = this.getEmbeddedServletContainerFactory();
- 使用容器工厂获取嵌入式Servlet容器 getEmbeddedServletContainer
- 嵌入式Servlet容器被创建且启动
- 先启动嵌入式Servlet容器,再将IoC容器中剩下没被创建的对象创建出来
5. 使用外置Servlet容器
- 嵌入式Servlet容器:打包为jar
优点:简单、便携;
缺点:默认不支持JSP, 优化机制比较复杂
使用定制器[ServerProperties/自定义EmbeddedServletContainerCustomizer]
自己编写嵌入式Servlet容器的创建工厂[EmbeddedServletContainerFactory] -
使用外置Servlet容器支持JSP:打包为war
(1)IDEA中如何生成Web目录结构即文件夹webapp+部署描述符web.xml?
打开ProjectStructure,在Module页下进行创建与路径指定
(2)配置外部服务器,并部署Deployement
(3)必须编写一个SpringBootServletInitializer的子类,并重写configure()[由Spring Initializer自动生成]
【启动原理】public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { //传入SpringBoot应用的主程序才能正常启动 return application.sources(SpringBootWebOutApplication.class); } }
-
servlet3.1-[8.2.4 Shared libraries / runtimes pluggablity]
(1)服务器启动,会创建当前WebApplication里面每一jar的SerletContainerInitializer实例
(2)ServletContainerInitializer实现的全限类名放在jar包的META-INF/services目录下中的一个名为javax.servlet.ServletContainerInitializer的文件中
(3)可以使用@HandlersTypes,在WebApplication启动的时候加载被选中的类 -
流程:
(1)启动Tomcat容器
(2)Spring的web模块中存在文件org\springframework\spring-web\4.3.19.RELEASE\spring-web-4.3.19.RELEASE.jar!\META-INF\services\javax.servlet.ServletContainerInitializer,其内容为org.springframework.web.SpringServletContainerInitializer
(3)@HandlesTypes(WebApplicationInitializer.class):将所有WebApplicationInitialier相关“类型”(接口、抽象类、实现类)传入到onStartup(Set<Class<?>>),创建符合要求的WebApplicationInitializer对象
(4)每一个WebApplicationInitializer都调用自己的onStartup()
显然SpringBootServletInitializer会被创建实例,并调用onStartup()
(5)在调用onStartup()时,createRootApplicationContext创建“父容器”
(6)Spring应用被启动【由run()创建IoC容器】protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) { //1.创建SpringApplicationBuilder SpringApplicationBuilder builder = this.createSpringApplicationBuilder(); builder.main(this.getClass()); ApplicationContext parent = this.getExistingRootWebApplicationContext(servletContext); if (parent != null) { this.logger.info("Root context already created (using as parent)."); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, (Object)null); builder.initializers(new ApplicationContextInitializer[]{new ParentContextApplicationContextInitializer(parent)}); } builder.initializers(new ApplicationContextInitializer[]{new ServletContextApplicationContextInitializer(servletContext)}); builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class); //2.调用configure(),由子类重写,传入SpringBoot的主类 builder = this.configure(builder); builder.listeners(new ApplicationListener[]{new SpringBootServletInitializer.WebEnvironmentPropertySourceInitializer(servletContext, null)}); //3. 使用Builder创建Spring应用 SpringApplication application = builder.build(); if (application.getSources().isEmpty() && AnnotationUtils.findAnnotation(this.getClass(), Configuration.class) != null) { application.getSources().add(this.getClass()); } Assert.state(!application.getSources().isEmpty(), "No SpringApplication sources have been defined. Either override the configure method or add an @Configuration annotation"); if (this.registerErrorPageFilter) { application.getSources().add(ErrorPageFilterConfiguration.class); } //4. 启动Spring应用 return this.run(application); }
public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; FailureAnalyzers analyzers = null; this.configureHeadlessProperty(); SpringApplicationRunListeners listeners = this.getRunListeners(args); listeners.starting(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments); Banner printedBanner = this.printBanner(environment); context = this.createApplicationContext(); new FailureAnalyzers(context); this.prepareContext(context, environment, listeners, applicationArguments, printedBanner); this.refreshContext(context); this.afterRefresh(context, applicationArguments); listeners.finished(context, (Throwable)null); stopWatch.stop(); if (this.logStartupInfo) { (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch); } return context; } catch (Throwable var9) { this.handleRunFailure(context, listeners, (FailureAnalyzers)analyzers, var9); throw new IllegalStateException(var9); } }
-