4.SpringBoot Web开发

# 4.Web开发 ## 4.1 简介 **使用SpringBoot**
  1. 创建SpringBoot应用,选中需要的模块
  2. SpringBoot已经默认将这些场景配置好了,只需要在配置文件中指定少量的就可以运行
  3. 编写业务代码

4.2 SpringBoot对静态资源的映射规则

ResourceProperties中可以设置静态有关的参数,缓存时间等…


@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties implements ResourceLoaderAware {
	// 可以设置和静态有关的参数,缓存时间等。
}

WebMvcAuotConfiguration

		// 静态资源的映射
		@Override
		public void addResourceHandlers(ResourceHandlerRegistry registry) {
			if (!this.resourceProperties.isAddMappings()) {
				logger.debug("Default resource handling disabled");
				return;
			}
			Integer cachePeriod = this.resourceProperties.getCachePeriod();
			if (!registry.hasMappingForPattern("/webjars/**")) {
				customizeResourceHandlerRegistration(
						registry.addResourceHandler("/webjars/**")
								.addResourceLocations(
										"classpath:/META-INF/resources/webjars/")
						.setCachePeriod(cachePeriod));
			}
			String staticPathPattern = this.mvcProperties.getStaticPathPattern();
			// 映射静态资源
			if (!registry.hasMappingForPattern(staticPathPattern)) {
				customizeResourceHandlerRegistration(
						registry.addResourceHandler(staticPathPattern)
								.addResourceLocations(
										this.resourceProperties.getStaticLocations())
						.setCachePeriod(cachePeriod));
			}
		}
	// 映射欢迎页面
	@Bean
		public WelcomePageHandlerMapping welcomePageHandlerMapping(
				ResourceProperties resourceProperties) {
			return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(),
					this.mvcProperties.getStaticPathPattern());
		}
	// 配置喜欢的图标
	@Configuration
	@ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
	public static class FaviconConfiguration {

		private final ResourceProperties resourceProperties;
		public FaviconConfiguration(ResourceProperties resourceProperties) {
				this.resourceProperties = resourceProperties;
		}

		@Bean
		public SimpleUrlHandlerMapping faviconHandlerMapping() {
				SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
				mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
				mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
						faviconRequestHandler()));
			return mapping;
		}

		@Bean
		public ResourceHttpRequestHandler faviconRequestHandler() {
				ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
				requestHandler
						.setLocations(this.resourceProperties.getFaviconLocations());
			return requestHandler;
		}

	}

4.2.1 webjars

所有/webjars/**,都去classpath:/META-INF/resource/webjars/找资源;

webjars:以jar包的方式引入静态资源,webjars官网

示例,引入Jquery
在pom.xml中依赖jar

 <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

jquery部分源码

4.2.2 “/**”

"/**"访问当前项目的任何资源都去(静态资源文件夹)找映射

"classpath:/META-INF/resource/",
"classpath:/resource/",
"classpath:/static/"
"classpath:/public/"
"/:当前项目的根路径"

localhost:项目端口/abc就等同于去静态资源文件里面找abc

示例
访问静态资源
根据SpringBoot的映射规则"bootstartp.min.js"访问规则是

http://localhost:8080/asserts/js/bootstrap.min.js

4.2.3 欢迎页

private Optional<Resource> getWelcomePage() {
			// "classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/"
			String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
			// 循环添加上index.html
			return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
		}

private Resource getIndexHtml(String location) {
			return this.resourceLoader.getResource(location + "index.html");
}

在静态资源文件下的所有有的"index.html"页面,被"/**"映射;

示例

访问路径

http://localhost:8080/

改地址符合"/**"映射规则,并且由于没有指定路径所以默认去查找静态资源下的index.html

index.html
首页

4.2.4 favicon.ico

	@Bean
			public SimpleUrlHandlerMapping faviconHandlerMapping() {
				SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
				mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
				mapping.setUrlMap(Collections.singletonMap("**/favicon.ico", faviconRequestHandler()));
				return mapping;
			}

			@Bean
			public ResourceHttpRequestHandler faviconRequestHandler() {
				ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
				requestHandler.setLocations(resolveFaviconLocations());
				return requestHandler;
			}

			private List<Resource> resolveFaviconLocations() {
			// "classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/"
				String[] staticLocations = getResourceLocations(this.resourceProperties.getStaticLocations());
				List<Resource> locations = new ArrayList<>(staticLocations.length + 1);
				Arrays.stream(staticLocations).map(this.resourceLoader::getResource).forEach(locations::add);
				locations.add(new ClassPathResource("/"));
				return Collections.unmodifiableList(locations);
			}

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

示例

往任意一个静态资源文件夹放入favicon.ico文件

ico文件

再次访问

baidu.ico

4.2.5 staticLocations

在SpringBoot中可以通过spring.resources.static-locations来修改默认静态资源映射文件
staticLocations
示例

修改静态资源文件默认映射的地址添加classpath:hello/

spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/,classpath:hello/

删除其他静态文件中的index.html,添加index.html文件到classpath:hello/中

自定义staticLocations
还是存在之前的默认配置的效果说明自定义staticLocations成功.

自定义静态资源文件映射

4.3 模版引擎

常见的模版引擎有,JSP、Velocity、Freemarker、Thymeleaf

模版引擎存在语法和实现的不同但是思想都是一致的.

模版引擎

SpringBoot推荐的Thymeleaf

语法更简单,功能更强大

4.3.1 引入thymeleaf

1. 引入thymeleaf

在pom文件中添加依赖

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
  </dependency>

设置thymeleaf的版本

        <thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
        <!--   布局功能的支持程序 thymeleaf3主程序 layout2以上版本     -->
        <!-- thymeleaf2   layout1-->
        <thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>

注意

如果存在中间的适配包一定要参考官方的文档说,如:thymeleaf3主程序 适配layout2以上版本

2. Thymeleaf
2.1 自动渲染
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {

	private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8");

	private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html");

	public static final String DEFAULT_PREFIX = "classpath:/templates/";

	public static final String DEFAULT_SUFFIX = ".html";

在SpringBoot下只要把HTML页面放在classpath:/templates/并且thymeleaf也能自动的渲染.

示例

  1. 编写一个控制器跳转到success.html页面

@Controller
public class HelloController {
    @RequestMapping("/success")
    public String success(){
        //classpath:/templates/success.html
        return "success";
    }
}
  1. 将success.html文件放在classpath:/templates/下
    success.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>成功</h1>
</body>
</html>
  1. 运行项目并输入链接测试
http://localhost:8080/success

success.html

2.2 使用步骤
  1. 导入thymeleaf的空间
<html lang="en"  xmlns:th="http://www.thymeleaf.org">

注意
如果不导入该空间的话则不会有提示,但是不影响使用.

  1. 使用thymeleaf语法
  @RequestMapping("/success")
    public String success(Map<String,Object> map){
        map.put("hello","你好");
        return "success";
    }

将查询出来的数据展示在前台

<!DOCTYPE html >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>成功</h1>
<!--th:text 将div里面的文本内容设置为-->
<div th:text="${hello}"></div>
</body>
</html>

success.html
注意

th:text会强制替换元素中的文本,如:上面的示例即使div中文本不是为空,经过thymeleaf的渲染也会被替换掉!

2.3 基础语法
1. 变量表达式 ${}

使用方法:直接使用 th:html元素属性。如:

    @RequestMapping("/")
    public String index(Model model) {
        UserEntity user = new UserEntity(1, "admin", "123456");
        model.addAttribute("user", user);
        return "index";
    }
<form id="form">
    <input id="id" name="id" th:value="${user.id}"/>
    <input id="username" name="username" th:value="${user.username}"/>
    <input id="password" name="password" th:value="${user.password}"/>
</form>
<div th:text="hello"></div>
<div th:text="${user.username}"></div>

变量表达式

2. 选择变量表达式 *{}

使用方法:通过th:object获取对象,然后使用th:xx="*{}"获取对象属性。如:

<form id="form" th:object="${user}">
    <input id="id" name="id" th:value="*{id}"/>
    <input id="username" name="username" th:value="*{username}"/>
    <input id="password" name="password" th:value="*{password}"/>
</form>
3.链接表达式@{}

通过链接表达式@{}直接拿到应用路径,然后拼接静态资源路径 如:

<script th:src="@{/webjars/jquery/3.4.1/jquery.js}"></script>
4. 片段表达式 ~{}

片段表达式是Thymeleaf的特色之一,细粒度可以达到标签级别,这是JSP无法做到的。片段表达式拥有三种语法。

~{veiwName}
表示引入完整的页面

~{viewName::selector}
表示在指定的页面寻找片段,其中selector可为片段名、jquery选择器等

~{::selector}

表示在当前页寻找

使用方式:首先通过th:fragment定制片段,然后通过th:replace填写片段路径和片段名,如:

head.html

<head th:fragment="static">
    <meta charset="UTF-8">
    <title>首页</title>
    <script th:src="@{/webjars/jquery/3.4.1/jquery.js}"></script>
</head>

index.html

<div th:replace="~{head::static}"></div>

注意
使用替换路径th:replace开头请勿添加斜杆,避免避免部署运行的时候出现路径报,因为默认拼接的路径为spring.thymeleaf.prefix = classpath:/templates/)

5. 消息表达式

即通常的国际化属性:#{msg} 用于获取国际化语言翻译值。如:

<title th:text="#{user.title}"></title>
6. 其它表达式

在基础语法中,默认支持字符串连接、数学运算、布尔逻辑和三目运算等。例如:

<input name="name" th:value="${'I am '+(user.name!=null?user.name:'NoBody')}"/>
2.4 七大基础对象

${#ctx}
上下文对象,可用于获取它内置对象。

${#vars}

上下文变量。

${#locale}

上下文区域变量

${#requst}

HttpServletRequsts对象

${#response}

HttpServletResponse对象

${#session}

HttpSession对象

${#servletContenxt}

SeveletContext对象

2.5 常用的工具类

#strings

字符串工具类

#lists

List工具类

#arrays
数组工具类

#sets
Set工具类

#maps
常用Map方法

#objects
一般对象类,通常用来判断非空

#bools
常用的布尔方法

#execInfo

获取页面模版处理信息

#messages

在变量表达式中获取外部消息的方法,与使用#{…}语法获取的方法相同。

#uris

转义部分URL/URI的方法

#conversions

用于执行以配置的转换服务方法。

#dates

时间操作和时间格式化。

#calendars

用于更复杂时间的格式化等。

#numbers

格式化数字对象的方法

#aggregates

在数组或集合上创建聚合的方法

#ids

处理可能重复的id属性的方法

2.6 迭代循环

想要遍历List集合很简单,配合th:each 即可快速完成迭代。如:

<form id="form" th:each="user:${list}">
    <input id="id" name="id" th:value="${user.id}"/>
    <input id="username" name="username" th:value="${user.username}"/>
    <input id="password" name="password" th:value="${user.password}"/>
</form>

在集合的迭代过程还可以获取状态变量,只需在变量后指定状态变量名即可。状态变量可以用于获取集合的下标/序号、总数、是否为单数是否为第一个/最后一个 如:

<form id="form" th:each="user,stat:${list}" th:class="${stat.even}?'even':'odd'">
    下标:<input th:value="${stat.index}">
    序号:<input th:value="${stat.count}"/>
    账号:<input th:value="${user.username}"/>
    密码:<input th:value="${user.password}"/>
</form>

如果是缺省状态变量名,则迭代器会默认帮我们生成以变量名开头的状态名。如:

<form id="form" th:each="user:${list}" th:class="${userStat.even}?'even':'odd'">
    下标:<input th:value="${userStat.index}">
    序号:<input th:value="${userStat.count}"/>
    账号:<input th:value="${user.username}"/>
    密码:<input th:value="${user.password}"/>
</form>
2.7 条件判断

条件判断通常用于动态页面的初始化。例如:

<div th:if="${list}">
    存在用户列表
</div>

如果想要取反则使用unless。如:

<div th:unless="${list}">
    不存在用户列表
</div>
2.8 日期格式化

使用默认的日期格式(toString方法) 并不是我们预期的格式:Mon Dec 03 23:16:50 CST 2018

<form id="form" th:each="user:${list}" th:class="${userStat.even}?'even':'odd'">
    下标:<input th:value="${userStat.index}">
    序号:<input th:value="${userStat.count}"/>
    账号:<input th:value="${user.username}"/>
    密码:<input th:value="${user.password}"/>
    时间:<input th:value="${user.createDate}"/>
</form>

此时可以通过时间工具类#dates来对日期进行格式化:2018-12-03 23:16:50

<form id="form" th:each="user:${list}" th:class="${userStat.even}?'even':'odd'">
    下标:<input th:value="${userStat.index}">
    序号:<input th:value="${userStat.count}"/>
    账号:<input th:value="${user.username}"/>
    密码:<input th:value="${user.password}"/>
    时间:<input th:value="${#dates.format(user.createDate,'yyyy-MM-dd HH:mm:ss')}"/>
</form>
2.9 内联写法
  • (1)为什么要使用内联写法?·答:因为 JS无法获取服务端返回的变量。
  • (2)如何使用内联表达式?答:标准格式为:[[${xx}]] ,可以读取服务端变量,也可以调用内置对象的方法。例如获取用户变量和应用路径:
<script th:inline="javascript">
    let userList=[[${list}]]
    let appPath=[[${#request.getContextPath()}]];
    let langCountry=[[${#locale.getLanguage()+'_'+#locale.getCountry()}]]
    console.log(userList)
    console.log(appPath)
    console.log(langCountry)
</script>

4.3 SpringMVC自动配置

4.3.1 Spring MVC auto-configuration

SpringBoot自动配置好了SpringMvc,一下是官方介绍, 官方文档


29.1.1 Spring MVC Auto-configuration
// Spring MVC 自动配置
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
// Spinrg Boot 为Spring MVC 提供了自动配置,适用于大多数应用程序
The auto-configuration adds the following features on top of Spring’s defaults:
// 自动配置默认添加了如下的 Spring 特性配置
    Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
    Support for serving static resources, including support for WebJars (covered later in this document)).
    Automatic registration of Converter, GenericConverter, and Formatter beans.
    Support for HttpMessageConverters (covered later in this document).
    Automatic registration of MessageCodesResolver (covered later in this document).
    Static index.html support.
    Custom Favicon support (covered later in this document).
    Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.
 Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans
  • 自动配置了ViewResolver(视图解析:根据方法的返回值得到视图对象(View),视图对象决定如何渲染)
  • ContentNegotiatingViewResolver :组合所有的视图解析的;
  • 如何定制:可以自己给容器中添加一个视图解析器,自动的将其组合进来

示例:自定义viewResolver

    @Bean
    public ViewResolver myResolver() {
        return new MyViewResolver();
    }

    private static class MyViewResolver implements ViewResolver {
        @Override
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            return null;
        }
    }

添加完成后就可以在,org.springframework.web.servlet.DispatcherServlet#doDispatch 方法中找到该视图解析器
MyViewResolver

Support for serving static resources, including support for WebJars
  • 静态资源文件夹路径webjars
 Static index.html support.
  • 静态首页访问
Custom Favicon support 
  • 自定义 favicon.ico
   Automatic registration of Converter, GenericConverter, and Formatter beans.
  • 自动注册了 Converter,GenericConverter 和 Formatter beans
  • Converter:转换器,用于页面提交数据时的类型转换,如提交的字符串“18”后台接收时转为Integer类型
  • Formatter :格式化器;,用于格式化页面提交的日期数据,如“2017-12-17”接收后要转为Date类型数据
  • GenericConverter:同样是转换器
	@Bean
	@ConditionalOnProperty(prefix = "spring.mvc", name = "date-format")
	public Formatter<Date> dateFormatter() {
			return new DateFormatter(this.mvcProperties.getDateFormat());
	}

说明
@ConditionalOnProperty(prefix = “spring.mvc”, name = “date-format”)

表示配置了"spring.mvc"中的date-form属性则生效

  • 自定义添加格式化转换器,只需要放在容器中即可
 Support for HttpMessageConverters
  • HttpMessageConverters :SpringMVC用来转换Http请求和响应的;
  • HttpMessageConverters:是从容器中确定;获取所有的HttpMessageConverter
  • SpringMVC 用来转换Http请求和响应的,如控制器方法以Json数据格式返回一个User对象给前端页面
  • 自定义HttpMessageConverters:自己给容器添加HttpMessageConverter,只需要将自己的组件注册容器中(@Bean,@Compoent)
 Automatic registration of MessageCodesResolver
  • 自定义错误代码生成的规则
 Automatic use of a ConfigurableWebBindingInitializer bean 
  • 初始化WebDataBinder,可用于将页面请求数据直接封装成JavaBean

4.3.2 扩展SpringMVC

  1. 需求:页面输入“http://localhost:8080/home” 后进入“home.html”页面

  2. 页面输入“http://localhost:8080/hello” 后进入“home.html”页面

  3. 所有页面请求都要经过自定义“LoginInterceptor” 拦截器进行拦截

SpringMvc的实现流程

  1. 编写拦截器
public class LoginInterceptor  implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        System.out.println(new Date()+"----------拦截器拦截前"+httpServletRequest.getRequestURI());
        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 {
        System.out.println(new Date()+"----------拦截器拦截后");
    }
}

  1. 编写SpringMvc的配置文件
 <!--配置拦截器-->
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="com.study.springboot.intercetors.LoginInterceptor"></bean>
        </mvc:interceptor>
    </mvc:interceptors>
    <!-- 当页面输入"localhost:8080/应用上下文路径/hello"之后,跳转到 classpath:/templates/home.html 页面-->
    <mvc:view-controller path="/hello" view-name="/home"/>
  1. 导入SpringMvc的配置文件

使用@ImportResource 导入配置文件

@ImportResource(value = "classpath:application-context.xml")
  1. 运行项目并输入连接测试
http://localhost:8080/home

home
这是HomeContorller中的映射

http://localhost:8080/hello

home
这是SpringMvc中的配置,页面跳转是完全没问题的,但是数据完全没过来.

SpringBoot的实现流程

注意
注意如果是和上面配置的在同一个项目,需要删除SpringMvc的配置文件,删除启动类上的@ImportResource

  1. WebMvcConfigurationSupport

编写自定义配置类并继承WebMvcConfigurationSupport ,注意不加 @EnableWebMvc,然后重写指定的方法即可扩展自动配置;

既保留了所有的自动配置,也能用自己扩展的配置,这是推荐方式

/**
 * 针对应用中 Spring MVC 扩展的配置类---实现 WebMvcConfigurerAdapter
 */
@Configuration
public class MyMvcConfig extends WebMvcConfigurer{

      /**
       *  <p>添加拦截器</p>
       */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor());
    }
     /**
      *  <p>添加视图控制器映射</p>
      */
    @Override
    protected void addViewControllers(ViewControllerRegistry registry) {
        // 当页面输入"localhost:8080/应用上下文路径/hello"之后,跳转到 classpath:/templates/home.html 页面
        registry.addViewController("/hello").setViewName("home");
    }
}
  1. 运行项目并输入连接测试
http://localhost:8080/home

home

这是HomeContorller中的映射

http://localhost:8080/hello

home
这和原本SpringMvc中的配置有着一样的问题,页面跳转是完全没问题的,但是数据完全没过来.

原理

  1. WebMvcAutoConfiguration是SpringMVC的自动配置类;
  2. 自动配置时会导入;@Import(EnableWebMvcConfiguration.class);
  3. 该类继承,DelegatingWebMvcConfiguration;
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration{}
  1. 在实例化EnableWebMvcConfiguration的时候 调用DelegatingWebMvcConfiguration中得到setConfigurers方法;
 //从容器中获取所有的WebMvcConfigurer
	@Autowired(required = false)
	public void setConfigurers(List<WebMvcConfigurer> configurers) {
		if (!CollectionUtils.isEmpty(configurers)) {
			this.configurers.addWebMvcConfigurers(configurers);
		}
	}
  1. 容器中的WebMvcConfigurers(包括自定义的配置文件)都会起作用了;

4.3.3 全面接管SpringMVC

SpringBoot对SpringMvc的自动配置不需要了,所有都是自己配置.

只需要在配置类中添加@EnableWebMvc即可

@EnableWebMvc
@Configuration
public class MyMvcConfig extends WebMvcConfigurationSupport {}

为什么@EnableWebMvc自动配置就失效了

  1. @EnableWebMvc
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}
  1. DelegatingWebMvcConfiguration
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {}
  1. WebMvcAutoConfiguration
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
      WebMvcConfigurerAdapter.class })
//容器没有这个组件的时候,这个自动配置类才生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
      ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
  1. @EnableWebMvc将WebMvcConfigurationSupport导入进来了;
  2. 导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能

4.3.4 修改SpringMVC默认配置

模式:

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

4.4 RestfulCRUD

4.4.1 默认访问首页

在配置类中编写配置类

	// 所有的WebMvcConfigurer都会起作用
    @Bean // 注册到容器里
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            // 添加视图解析
            @Override
            public void addViewControllers(ViewControllerRegistry registry) {
                registry.addViewController("/").setViewName("/login.html");
            }
        };
    }

静态资源的引用

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

4.4.2 国际化

  1. 编写国际化文件
  2. 使用ResourceBundleMessageSource管理国际化资源文件
  3. 在页面中使用fmt:message,取出国际化内容
1.浏览器切换国际化

步骤

  1. 编写国际化配置文件,抽取页面需要的显示的国际化消息

zh_CN

login.btn=登陆
login.password=密码
login.rememberme=记住我
login.tip=请登录
login.username=用户名

en_US

login.btn=Sign In
login.password=password
login.rememberme=Remember Me
login.tip=Please sign in
login.username=UserName
  1. SpringBoot自动配置好了国际化配置的资源文件
@Configuration
@ConditionalOnMissingBean(value = MessageSource.class, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {
	@Bean
	public MessageSource messageSource(MessageSourceProperties properties) {
		ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
		if (StringUtils.hasText(properties.getBasename())) {
		//设置国际化文件的基础名,去掉语言国家代码
			messageSource.setBasenames(StringUtils
					.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
		}
		if (properties.getEncoding() != null) {
			messageSource.setDefaultEncoding(properties.getEncoding().name());
		}
		messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
		Duration cacheDuration = properties.getCacheDuration();
		if (cacheDuration != null) {
			messageSource.setCacheMillis(cacheDuration.toMillis());
		}
		messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
		messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
		return messageSource;
	}
	// 资源文件可以直接丢在类路径下
	private Resource[] getResources(ClassLoader classLoader, String name) {
			String target = name.replace('.', '/');
			try {
				return new PathMatchingResourcePatternResolver(classLoader)
						.getResources("classpath*:" + target + ".properties");
			}
			catch (Exception ex) {
				return NO_RESOURCES;
			}
		}
	
}
# 自定义资源文件夹的位置
 spring.messages.basename=i18n.login
  1. 使用资源文件(login进行标签插入)
<!DOCTYPE html>
<!-- saved from url=(0051)https://getbootstrap.com/docs/4.1/examples/sign-in/ -->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="" />
    <meta name="author" content="" />
    <link rel="icon" href="https://getbootstrap.com/favicon.ico" />

    <title>登录页面</title>

    <!-- Bootstrap core CSS -->
    <link href="#" th:href="@{/css/bootstrap.min.css}" rel="stylesheet" />

    <!-- Custom styles for this template -->
    <link href="./login_files/signin.css" th:href="@{/css/signin.css}" rel="stylesheet" />
  </head>

  <body class="text-center">
    <form class="form-signin">
      <img class="mb-4" src="./login_files/bootstrap-solid.svg" th:src="@{/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"  name="username" class="form-control" placeholder="Username" th:placeholder="#{login.username}" required="" autofocus=""/>
      <label for="inputPassword" class="sr-only" th:text="#{login.password}">Password</label>
      <input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" th:placeholder="#{login.password}" required="" />
      <div class="checkbox mb-3">
        <label>
          <input type="checkbox" value="remember-me" /> [[#{login.remember}]]
        </label>
      </div>
      <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
      <p class="mt-5 mb-3 text-muted">© 2017-2018</p>
    </form>
  

</body></html>

效果根据浏览器语言的信息切换国际化

原理

默认的就是根据请求头带来的区域信息获取local国际化信息

		@Bean
		@ConditionalOnMissingBean
		@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
		public LocaleResolver localeResolver() {
			if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
				return new FixedLocaleResolver(this.mvcProperties.getLocale());
			}
			AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
			localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
			return localeResolver;
		}
2.浏览器切换国际化

自己编写localResolver,加到容器中

  1. 更改HTML代码
<a class="btn btn-sm" th:href="@{/index.html(lang='zh_CN')}">中文</a>
<a class="btn btn-sm"  th:href="@{/index.html(lang='en_US')}">English</a>
  1. 新建一个MyLocaleResolver.class
public class MyLocaleResolver implements LocaleResolver {

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        // 获取参数
        String lang = request.getParameter("lang");
        Locale locale = Locale.getDefault();
        if (!StringUtils.isEmpty(lang)) {
            String[] split = lang.split("_");
            locale = new Locale(split[0], split[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}
  1. 将MyLocaleResolver加入到容器中
 @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
3.登录拦截器
3.1 登录

1. 开发技巧

  1. 清除模板缓存
# 禁用模版引擎的缓存
spring.thymeleaf.cache=false
  1. Ctrl+F9刷新

2.编写LoginController

@Controller
public class LoginController {
    
    @PostMapping("/user/login")
    public String login(@RequestParam("username") String userName
            , @RequestParam("password") String password
            , Map<String, Object> map, HttpSession session) {
        if (!StringUtils.isEmpty(userName) && "123456".equals(password)) {
            return "dashboard";
        } else {
            // 登录失败
            map.put("msg", "用户名密码错误");
            return "login";
        }
    }
}

3.登录错误消息显示

<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

4.防止重复提交

if (!StringUtils.isEmpty(userName) && "123456".equals(password)) {
            // 为了防止浏览器的重复提交,重定向到main.html
            return "redirect:/main.html";
        } else {
            // 登录失败
            map.put("msg", "用户名密码错误");
            return "login";
  		}

5.添加视图映射

   
@Override
public void addViewControllers(ViewControllerRegistry registry) {
      registry.addViewController("/").setViewName("/login.html");
      registry.addViewController("/index.html").setViewName("login");
      registry.addViewController("/main.html").setViewName("dashboard");
 }
3.2 拦截器

作用

实现权限控制,每个页面请求前中后,都会进入到拦截器进行处理(登录权限)

1.创建LoginHandleInterceptor拦截器

/**
 * 登录检查
 */
public class LoginHandleInterceptor implements HandlerInterceptor {
    /**
     * 目标方法执行之前
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object user = request.getSession().getAttribute("loginUser");
        if (user != null) {
            // 已登录
            return true;
        } else {
            // 未登录
            request.setAttribute("msg", "没有权限,请先登录");
            request.getRequestDispatcher("/index.html").forward(request, response);
            return false;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

2.在配置中重写拦截器方法,加入到容器中

//@EnableWebMvc
@Configuration // 不要全面接管SpringMvc
public class MyMvcConfig implements WebMvcConfigurer {


    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            // 添加视图解析
            @Override
            public void addViewControllers(ViewControllerRegistry registry) {
                registry.addViewController("/").setViewName("/login.html");
                registry.addViewController("/index.html").setViewName("login");
                registry.addViewController("/main.html").setViewName("dashboard");
            }
        };
    }

    @Bean
    public LocaleResolver localeResolver() {
        return new MyLocaleResolver();
    }

    /**
     * 注册拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注意放行,静态资源 *.css、*.js、*.html
        registry.addInterceptor(new LoginHandleInterceptor()).addPathPatterns("/**")
                .excludePathPatterns("/index.html", "/", "/user/login","/asserts/**","/webjars/**");
    }
}

3.在LoginHandler中添加登录成功写入session

    @PostMapping("/user/login")
    public String login(@RequestParam("username") String userName
            , @RequestParam("password") String password
            , Map<String, Object> map, HttpSession session) {
        if (!StringUtils.isEmpty(userName) && "123456".equals(password)) {
            // 为了防止浏览器的重复提交,重定向到main.html
            session.setAttribute("loginUser", userName);
            return "redirect:/main.html";
        } else {
            // 登录失败
            map.put("msg", "用户名密码错误");
            return "login";
        }
    }
4.CRUD-员工列表
4.1 实验要求

1. RestfulCRUD:

CRUD满足Rest风格URI:/资源名称/资源标识+HTTP操作

普通CRUDRestfulCRUD
查询getEmpemp – GET
添加addEmp?xxxemp --POST
修改updateEmp?id=xxx&xxx=xxemp/{id} – PUT
删除deleteEmp?id=1emp/{id} --DELETE

2.实验的请求架构

请求URI请求方式
查询所有员工empsGET
查询某个员工emp/{id}GET
添加页面empPOST
修改页面(回显)emp/{id}GET
修改员工emp/{id}PUT
删除员工emp/{id}DELETE
4.2 页面抽取

1.公共页面抽取

使用方法

  1. 抽取公共片段
<!--footer.html-->
<div id="footid" th:fragment="copy">xxx</div>
  1. 引入公共片段
<!--test.html-->
<div th:insert=~{footer::copy}></div>

说明

~{templatename::selector} 模板名::选择器 footer::#footid
~{templatename::fragmentname} 模板名::片段名称 footer::copy
行内写法可以加~{xx::xx} 标签体可以 xx::xx

2.三种引用方式

  1. th:insert :加个外层标签 +1
  2. th:replace :完全替换 1
  3. th:include:就替换里面的内容 -1

示例

公共页面

<body>
	...
    <div th:insert="footer :: copy"></div>
    <div th:replace="footer :: copy"></div>
    <div th:include="footer :: copy"></div>
</body>

结果

<body>
...
    <!-- th:insert -->
    <div>
        <footer>
            &copy; 2011 The Good Thymes Virtual Grocery
        </footer>
    </div>
    <!--th:replace-->
    <footer>
   		&copy; 2011 The Good Thymes Virtual Grocery
    </footer>
    <!--th:include-->
    <div>
        &copy; 2011 The Good Thymes Virtual Grocery
    </div>
</body>

用此种方法将公共页面引入

3.列表高亮

引入片段的时候传入参数,新建一个commons文件夹存储公共页面bar.html

模板引入变量名

dashboard

<a class="nav-link active"
   th:class="${activeUri}=='main.html'?'nav-link active':'nav-link'"
   href="https://getbootstrap.com/docs/4.1/examples/dashboard/#" th:href="@{/main.html}">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
    Dashboard <span class="sr-only">(current)</span>
</a>

员工管理

   <a class="nav-link"
       th:class="${activeUri}=='emps'?'nav-link active':'nav-link'"
       href="https://getbootstrap.com/docs/4.1/examples/dashboard/#" th:href="@{/emps}">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
        员工管理
    </a>

引入模板的时候传入参数

dashboard.html引入

<!--引入侧边栏-->
<div th:replace="commons/bar :: sidebar(activeUri='main.html')"></div>

list.html引入

<!--引入侧边栏-->
<div th:replace="commons/bar::sidebar(activeUri='emps')"></div>
5. 列表数据显示(查)
5.1 传入员工对象
@Controller
public class EmployeeController {
    @Autowired
    private EmployeeDao employeeDao;

    @GetMapping("/emps")
    public String list(Model model) {
        // thymeleaf classpath:/template/xxx.html
        Collection<Employee> employees = employeeDao.getAll();
        model.addAttribute("emps", employees);
        return "emp/list";
    }
}
5.2 遍历对象
<table class="table table-striped table-sm">
                    <thead>
                    <tr>
                        <th>id</th>
                        <th>lastName</th>
                        <th>email</th>
                        <th>gender</th>
                        <th>departmentName</th>
                        <th>birth</th>
                        <th>操作</th>
                    </tr>
                    </thead>
                    <tbody>
                    <tr th:each="emp:${emps}">
                        <td>[[${emp.id}]]</td>
                        <td>[[${emp.lastName}]]</td>
                        <td>[[${emp.email}]]</td>
                        <td>[[${emp.gender}==0?'女':'男']]</td>
                        <td>[[${emp.department.departmentName}]]</td>
                        <td>[[${#dates.format(emp.birth,'yyyy/MM/dd HH:ss:mm')}]]</td>
                        <td>
                            <button class="btn btn-sm btn-primary">编辑</button>
                            <button class="btn btn-sm btn-danger">删除</button>
                        </td>
                    </tr>
                 </tbody>
</table>
5.3 效果

员工查询列表

6. 员工添加(增)

功能:点击添加按钮,出现新增页面

6.1 新增页面
<form>
    <!-- LastName -->
    <div class="form-group">
        <label for="LastName">LastName</label>
        <input type="text" class="form-control" id="LastName"  placeholder="LastName">
    </div>
    <!-- Email -->
    <div class="form-group">
        <label for="Email">Email</label>
        <input type="email" class="form-control" id="Email"  placeholder="zhangsan@163.com">
    </div>
    <!--gender-->
    <div class="form-group">
        <label >Gender</label><br/>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="gender" value="1">
            <label class="form-check-label" ></label>
        </div>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="gender" value="0">
            <label class="form-check-label" ></label>
        </div>
    </div>
    <!-- department -->
    <div class="form-group">
        <label for="exampleFormControlSelect1">department</label>
        <select class="form-control" id="exampleFormControlSelect1">
            <option th:each="dept:${depts}" th:value="${dept.id}" th:text="${dept.departmentName}"></option>
        </select>
    </div>
    <!--Birth-->
    <div class="form-group">
        <label for="birthDate">Birth</label>
        <input type="text" class="form-control" id="birthDate" placeholder="2012-12-12">
    </div>
    <button type="submit" class="btn btn-primary">添 加</button>
</form>
6.2 跳转页面
    /**
     * 来到添加页面
     */
    @GetMapping("/emp")
    public String add(Model model) {
        // 来到添加页面之前,查出所有的部门
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("depts", departments);
        return "emp/add";
    }

关键点:在添加部门页面要遍历部门信息,所以在方法中出入部门信息

6.3添加功能完成

新建一个PostMapping

1.新建一个postMapping的方法用来接受页面的添加POST请求

 /**
     * 员工添加
     * SpringMVC自动请求参数和入参对象的属性进行绑定一一绑定,要求请求参数的名字和
     * javaBean入参的对象属性名一致
     */
    @PostMapping("/emp")
    public String add(Employee employee) {
        // 来到员工列表页
        // redirect:表示重定向到一个地址,/表示当前页面
        // forward:表示转发到一个地址
        System.out.println("保存的员工信息:" + employee);
        //保存员工
        employeeDao.save(employee);
        return "redirect:emps";
    }

2.修改添加页面,添加name属性

<form th:action="@{/emp}" method="post">
    <!-- LastName -->
    <div class="form-group">
        <label for="LastName">LastName</label>
        <input type="text" class="form-control" id="LastName" name="lastName" placeholder="LastName">
    </div>
    <!-- Email -->
    <div class="form-group">
        <label for="Email">Email</label>
        <input type="email" class="form-control" id="Email"  name="email" placeholder="zhangsan@163.com">
    </div>
    <!--gender-->
    <div class="form-group">
        <label >Gender</label><br/>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="gender" value="1">
            <label class="form-check-label" ></label>
        </div>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="gender" value="0">
            <label class="form-check-label" ></label>
        </div>
    </div>
    <!-- department -->
    <div class="form-group">
        <label >department</label>
        <select class="form-control"  name="department.id">
            <option th:each="dept:${depts}" th:value="${dept.id}" th:text="${dept.departmentName}"></option>
        </select>
    </div>
    <div class="form-group">
        <label for="birthDate">Birth</label>
        <input type="text" class="form-control" id="birthDate" placeholder="2012-12-12" name="birth">
    </div>
    <button type="submit" class="btn btn-primary">添 加</button>
</form>

3.日期格式化处理

SpringMvc中添加 date-formate 默认是" /"格式的日期

可以通过"spring.mvc.date-format"属性修改格式化规则

 spring.mvc.date-format=yyyy-MM-dd
6.4员工编辑

思路使用add页面,并且数据回显,然后区分添加,设置PUT请求

1.在list页面上编辑按钮加上链接

  <td>
        <a class="btn btn-sm btn-primary" th:href="@{emp/}+${emp.id}">编辑</a>
        <button class="btn btn-sm btn-danger">删除</button>
 </td>

2.编写跳转页面

@GetMapping("emp/{id}")
    public String edit(@PathVariable("id") Integer id, Model model) {
        Employee employee = employeeDao.get(id);
        model.addAttribute("emp", employee);
        // 来到修改页面之前,查出所有的部门
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("depts", departments);
        // 回到修改页面(add是一个修改、添加二合一的页面)
        return "emp/add";
    }

3.对页面修改

           <form th:action="@{/emp}" method="post">
                <!-- 发送put请求修改员工数据 -->
                <!--
                    1.SpringMvc中配置HiddenHttpMethodFilter(SpringBoot自动配置好的)
                    2.页面创建一个POST表单
                    3.创建一个input项,name="_method",值就是我们指定的请求方式
                 -->
                <input type="hidden" name="_method" value="put" th:if="${emp!=null}">
                <input type="hidden" name="id" th:value="${emp.id}" th:if="${emp!=null}">
                <div class="form-group">
                    <label>LastName</label>
                    <input name="lastName" type="text" class="form-control" placeholder="lastName"
                           th:value="${emp!=null}?${emp.lastName}">
                </div>
                <div class="form-group">
                    <label>Email</label>
                    <input name="email" type="email" class="form-control" placeholder="email"
                           th:value="${emp!=null}?${emp.email}">
                </div>
                <div class="form-group">
                    <label>Gender</label><br/>
                    <div class="form-check form-check-inline">
                        <input class="form-check-input" type="radio" name="gender" value="1"
                               th:checked="${emp!=null}?${emp.gender==1}">
                        <label class="form-check-label"></label>
                    </div>
                    <div class="form-check form-check-inline">
                        <input class="form-check-input" type="radio" name="gender" value="0"
                               th:checked="${emp!=null}?${emp.gender==0}">
                        <label class="form-check-label"></label>
                    </div>
                </div>
                <div class="form-group">
                    <label>department</label>
                    <!--    提交部门的ID    -->
                    <select name="department.id" class="form-control">
                        <option th:selected="${emp!=null}?${emp.department.id==dept.id}" th:each="dept:${depts}"
                                th:text="${dept.departmentName}" th:value="${dept.id}"></option>
                    </select>
                </div>
                <div class="form-group">
                    <label>Birth</label>
                    <input name="birth" th:value="${emp!=null}?${#dates.format(emp.birth,'yyyy-MM-dd')}"
                           type="text"
                           class="form-control" placeholder="Birth">
                </div>
                <button type="submit" class="btn btn-primary" th:text="${emp!=null}?'修改':'添加'"></button>
            </form>

6.4员工删除

1.新建删除接口

  /**
     * 删除员工
     */
    @DeleteMapping("emp/{id}")
    public String delete(@PathVariable("id") Integer id) {
        employeeDao.delete(id);
        return "redirect:/emps";
    }

2.修改删除标签

 <button th:attr="delete_uri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger deleteBtn"
                                    type="button">删除
                            </button>

3.写Form表单

表单写在外面,input 中 name="_method" value=“delete” 模拟delete请求

<form id="deleteEmpForm" method="post">
    <input type="hidden" name="_method" value="delete">
</form>

4.写JS提交(Jquery)

    $(".deleteBtn").click(function () {
        var delete_uri = $(this).attr("delete_uri")
        $("#deleteEmpForm").attr("action", delete_uri)
        $("#deleteEmpForm").submit()
        return false;
    })

4.5 错误处理

4.5.1 SpringBoot默认的错误处理机制

1. 默认效果
1.1 浏览器

浏览器,返回一个默认的错误页面,包含:时间、类型、错误代码、说明

浏览器默认页面
浏览器发送请求的请求头

浏览器请求头

1.2 其他客户端

如果是其他客户端,默认响应一个json数据

{
    "timestamp": "2019-10-22T14:14:21.586+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/crud/111/"
}

在这里插入图片描述

2.原理

可以参照ErrorMvcAutoConfiguration;错误处理的自动配置;

org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration

在容器中自动添加了以下组件

  1. DefaultErrorAttributes
    帮我们在页面共享信息
	@Override
   public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
   	Map<String, Object> errorAttributes = new LinkedHashMap<>();
   	errorAttributes.put("timestamp", new Date());
   	addStatus(errorAttributes, webRequest);
   	addErrorDetails(errorAttributes, webRequest, includeStackTrace);
   	addPath(errorAttributes, webRequest);
   	return errorAttributes;
   }
  1. BasicErrorController

在未配置ErrorController的情况下处理默认错误请求

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {


   @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // 响应HTML文件
   public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
   	HttpStatus status = getStatus(request);
   	Map<String, Object> model = Collections
   			.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
   	response.setStatus(status.value());
   	// 去哪个页面作为错误页面;包含页面地址和页面内容
   	ModelAndView modelAndView = resolveErrorView(request, response, status, model);
   	return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
   }

   @RequestMapping // 响应json数据
   public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
   	Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
   	HttpStatus status = getStatus(request);
   	return new ResponseEntity<>(body, status);
   }
  1. ErrorPageCustomizer

系统出现错误以后来到ErrorPageCustomizer 来获取错误页面的实际路径 ${error.path}默认/error;

@Value("${error.path:/error}")
   private String path = "/error";//
  1. DefaultErrorViewResolver
@Override
   public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
   	ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
   	if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
   		modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
   	}
   	return modelAndView;
   }

   private ModelAndView resolve(String viewName, Map<String, Object> model) {
   // 默认SpringBoot可以去找到一个页面?error/404
   	String errorViewName = "error/" + viewName;
 	// 模版引擎可以解析这个页面地址可以解析这个页面地址就用模版引擎解析
   	TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
   			this.applicationContext);
   	if (provider != null) {
   	// 模版引擎可以用的情况下放回errorViewName指定地址
   		return new ModelAndView(errorViewName, model);
   	}
   	// 模版引擎不可以用,就在静态资源文件夹下找errorViewName对应的页面
   	return resolveResource(errorViewName, model);
   }
2.1 步骤

一旦系统出现4xx或者5xx之类的错误;ErrorPageCustomizer就会生效(定制错误的响应规则)默认来到 /error路径被BasicErrorController处理

  1. 响应页面;去哪个页面是有默认DefaultErrorViewResolver解析得到
	protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
			Map<String, Object> model) {
			// 所有的ErrorViewResolver
		for (ErrorViewResolver resolver : this.errorViewResolvers) {
			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
			if (modelAndView != null) {
				return modelAndView;
			}
		}
		return null;
	}
3.定制
3.1 如何定制错误页面
  1. 有模版引擎的情况下;
    error/状态码:将错误页面命名为 错误状态码.html放在error文件夹下,发送此状态码的错误就会来到 对应的页面,如果没有精确的错误码,可以使用4xx和5xx作为错误页面的来匹配这种类型的所有错误,注意这个匹配默认是精确优先,(优先寻找精确的错误的状态码.html)

页面能获取的错误信息:

  • timestamp:时间戳
  • status:状态码
  • error:错误提示
  • exception:异常对象
  • message:异常消息
  • errors:JSR303数据校验错误都在这里
  1. 没有模版引擎
    模版引擎找不到这个错误页面,静态资源文件下找。
  2. 默认都没有的情况下
    以上都没有错误页面,就是默认来到SpringBoot默认的错误提示页面。
3.2 如何定制错误的json数据
  1. 自定义异常处理&定制json数据
@ControllerAdvice
public class MyExceptionHandler {

    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map<String, Object> handleException(Exception e) {
        Map<String, Object> exceptionInfo = new HashMap<>();
        exceptionInfo.put("code", "user.notexist");
        exceptionInfo.put("message", e.getMessage());
        return exceptionInfo;
    }
}

不过还有一个缺陷,就是不管是浏览器访问还是其他访问方式都会返回JSON

  1. 改进将错误转发到/error交给默认处理,就会实现自适应
   @ExceptionHandler(UserNotExistException.class)
    public String handleException(Exception e, HttpServletRequest request) {
        Map<String, Object> exceptionInfo = new HashMap<>();
        // 传入我们自己的错误状态码 4xx 5xx,否则不会跳转到错误定制页面
        // Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        request.setAttribute("javax.servlet.error.status_code",500);
        exceptionInfo.put("code", "user.notexist");
        exceptionInfo.put("message", e.getMessage());
        return "forward:/error";
    }
  1. 将我们定制数据携带出去;

出现错误以后回来到,/error请求,会被BasicErrorController处理,响应出去可以活的数据是有
getErrorAttributes得到的(是AbstractErrorController(ErrorController的子类)规定的方法)

  • 完全编写ErrorController的实现类【或者编写AbstractErrorController的子类】,放在容器中;
  • 页面上能用的数据,或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes得到
    容器中DefaultErrorAttributes.getErrorAttributes();默认进行数据处理的;
    自定义ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        map.put("auth", "panHu");
        return map;
    }
}

终的效果:响应是自适应的,可以通过定制ErrorAttributes改变需要返回的内容。

@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        // 异常处理器携带的数据
        Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", ServletRequestAttributes.SCOPE_REQUEST);
        map.put("auth", "panHu");
        map.put("ext", ext);
        return map;
    }
}

json

{
    "timestamp": "2019-11-02T09:54:35.380+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "用户不存在!",
    "path": "/crud/hello",
    "auth": "panHu",
    "ext": {
        "code": "user.notexist",
        "message": "用户不存在!"
    }
}

html

页面错误

4.5 配置嵌入式Servlet容器

SpringBoot默认是用Tomcat嵌入的Serrvlet容器

依赖图

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

1. 修改Servlet有关的配置(ServerProperties[也是WebServerFactoryCustomizer])
server.servlet.context-path=/crud
server.port=8081
// 通用Servlet容器设置
server.xxx
// Tomcat的设置
server.tomcat.xxx
2. 编写一个 WebServerFactoryCustomizer

编写一个WebServerFactoryCustomizer嵌入式的Servlet容器定制器;来修改Servlet容器中的配置器

@Bean
    public WebServerFactoryCustomizer webServerFactoryCustomizer(){
        return new WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>(){
            // 定制嵌入式容器相关规则
            @Override
            public void customize(ConfigurableServletWebServerFactory factory) {
                factory.setPort(8083);
            }
        };
    }

4.5.2 注册Servlet、Filter、Listener

由于SpringBoot默认是jar包的方式启动嵌入式的Servlet容器来启动SpringBoot的web应用,没有web.xml文件。

注册组件用以下方式:

  • ServletRegistrationBean
  @Bean
    public ServletRegistrationBean myServlet(){
        ServletRegistrationBean servletRegistrationBean=new ServletRegistrationBean();
        servletRegistrationBean.setServlet(new MyServlet());
        servletRegistrationBean.addUrlMappings("/myServlet");
        return servletRegistrationBean;
    }
  • FilterRegistrationBean
    @Bean
    public  FilterRegistrationBean myFilter(){
        FilterRegistrationBean registrationBean=new FilterRegistrationBean();
        registrationBean.setFilter(new MyFilter());
        registrationBean.setUrlPatterns(Arrays.asList("/hello","/myServlet"));
        return registrationBean;
    }
  • ServletListenerRegistrationBean
   @Bean
    public ServletListenerRegistrationBean myListener(){
        ServletListenerRegistrationBean<MyListener> servletListenerRegistrationBean=new ServletListenerRegistrationBean<>();
        servletListenerRegistrationBean.setListener( new MyListener());
        return servletListenerRegistrationBean;
    }

SpringBoot帮我们自动SpringMVC的时候,自动的注册SpringMVC的前端控制器;DIspatcherServlet; DispatcherServletAutoConfiguration中:

	@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
		@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
		public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
			DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
					this.webMvcProperties.getServlet().getPath());
			//默认拦截器:/所有请求包括静态资源,但是不拦截jsp请求,/*会拦截jsp请求
			// 可以通过spring.mvc.servlet.path来修改SpringMVC前端控制器默认拦截的请求路径 
			registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
			registration.setLoadOnStartup(this.webMvcProperties.getServlet().getLoadOnStartup());
			if (this.multipartConfig != null) {
				registration.setMultipartConfig(this.multipartConfig);
			}
			return registration;
		}

4.5.3 使用其他Servlet容器

ConfigurableServletWebServerFactory

1.Tomcat(默认)
      <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
2. 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>
        <!--    引入jetty    -->
        <dependency>
            <artifactId>spring-boot-starter-jetty</artifactId>
            <groupId>org.springframework.boot</groupId>
        </dependency>
3. Undertow(不支持JSP)
 		<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>
        <!--    引入undertow    -->
        <dependency>
            <artifactId>spring-boot-starter-undertow</artifactId>
            <groupId>org.springframework.boot</groupId>
        </dependency>

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

@Configuration
class ServletWebServerFactoryConfiguration {
	
	@Configuration
	// 判断当前是否包含Tomcat依赖
	@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
	// 判断当前容器没有用户自己定义ServletWebServerFactory(Servlet容器工厂,作用:创建嵌入式的Servlet容器)
	@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
	public static class EmbeddedTomcat {

		@Bean
		public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
			return new TomcatServletWebServerFactory();
		}

	}
/**
	 * Nested configuration if Jetty is being used.
	 */
	@Configuration
	@ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })
	@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
	public static class EmbeddedJetty {

		@Bean
		public JettyServletWebServerFactory JettyServletWebServerFactory() {
			return new JettyServletWebServerFactory();
		}

	}

	/**
	 * Nested configuration if Undertow is being used.
	 */
	@Configuration
	@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
	@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
	public static class EmbeddedUndertow {

		@Bean
		public UndertowServletWebServerFactory undertowServletWebServerFactory() {
			return new UndertowServletWebServerFactory();
		}

	}
}
1. ServletWebServerFactory(web容器工厂)
@FunctionalInterface
public interface ServletWebServerFactory {

	// 获取Servlet容器
	WebServer getWebServer(ServletContextInitializer... initializers);

}

SpringBoot容器默认提供的Servlet容器

2. 以TomcatServletWebServerFactory为例
	@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
		// 创建一个Tomcat
		Tomcat tomcat = new Tomcat();
		// 配置Tomcat的基本环节
		File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
		tomcat.setBaseDir(baseDir.getAbsolutePath());
		Connector connector = new Connector(this.protocol);
		tomcat.getService().addConnector(connector);
		customizeConnector(connector);
		tomcat.setConnector(connector);
		tomcat.getHost().setAutoDeploy(false);
		configureEngine(tomcat.getEngine());
		for (Connector additionalConnector : this.additionalTomcatConnectors) {
			tomcat.getService().addConnector(additionalConnector);
		}
		prepareContext(tomcat.getHost(), initializers);
		//将配置好的tomcat传进去,返回一个tomcat容器,并且启动tomcat
		return getTomcatWebServer(tomcat);
	}
3. 我们对嵌入式容器配置修改是怎么生效的
ServerProperties、WebServerFactoryCustomizer

WebServerFactoryCustomizer;定制器帮我们修改了Servlet容器配置

  1. ServletWebServerFactoryAutoConfiguration
@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
		ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
		ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
		ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
// 1. 导入ServletWebServerFactoryConfiguration
// 2. 导入ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class
// 3. 导入WebServerFactoryCustomizerBeanPostProcessor
// 4. 后置处理器:bean初始化前后(创建完对象,还没属性赋值)执行初始化工作
public class ServletWebServerFactoryAutoConfiguration {

	@Bean
	public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) {
		return new ServletWebServerFactoryCustomizer(serverProperties);
	}

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

	/**
	 * Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via
	 * {@link ImportBeanDefinitionRegistrar} for early registration.
	 */
	public static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {

		private ConfigurableListableBeanFactory beanFactory;

		@Override
		public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
			if (beanFactory instanceof ConfigurableListableBeanFactory) {
				this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
			}
		}

		@Override
		public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
				BeanDefinitionRegistry registry) {
			if (this.beanFactory == null) {
				return;
			}
			registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor",
					WebServerFactoryCustomizerBeanPostProcessor.class);
			registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor",
					ErrorPageRegistrarBeanPostProcessor.class);
		}

		private void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name, Class<?> beanClass) {
			if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(beanClass, true, false))) {
				RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
				beanDefinition.setSynthetic(true);
				registry.registerBeanDefinition(name, beanDefinition);
			}
		}

	}

}
  1. 给容器导入,WebServerFactoryCustomizerBeanPostProcessor
@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		// 如果当前初始化的事WebServerFactory就调用postProcessBeforeInitialization
		if (bean instanceof WebServerFactory) {
			postProcessBeforeInitialization((WebServerFactory) bean);
		}
		return bean;
	}
	
	@SuppressWarnings("unchecked")
	private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {
		// 获取到每个定制器,并执行每一个定制的customize方法,来个Servlet赋值;
		LambdaSafe.callbacks(WebServerFactoryCustomizer.class, getCustomizers(), webServerFactory)
				.withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)
				.invoke((customizer) -> customizer.customize(webServerFactory));
	}
	private Collection<WebServerFactoryCustomizer<?>> getCustomizers() {
		if (this.customizers == null) {
			// Look up does not include the parent context
			// 在beanFactory中WebServerFactoryCustomizer类型获取bean集合
			// 定制Servlet容器,给容器中添加WebServerFactoryCustomizer类型的组件
			this.customizers = new ArrayList<>(getWebServerFactoryCustomizerBeans());
			this.customizers.sort(AnnotationAwareOrderComparator.INSTANCE);
			this.customizers = Collections.unmodifiableList(this.customizers);
		}
		return this.customizers;
	}
	
private Collection<WebServerFactoryCustomizer<?>> getWebServerFactoryCustomizerBeans() {
		return (Collection) this.beanFactory.getBeansOfType(WebServerFactoryCustomizer.class, false, false).values();
	}
  1. ServerProperties
    ServerProperties 通常是做为属性整合到WebServerFactoryCustomizer的实现

如:TomcatServletWebServerFactoryCustomizer中的customize方法

@Override
	public void customize(TomcatServletWebServerFactory factory) {
		ServerProperties.Tomcat tomcatProperties = this.serverProperties.getTomcat();
		if (!ObjectUtils.isEmpty(tomcatProperties.getAdditionalTldSkipPatterns())) {
			factory.getTldSkipPatterns().addAll(tomcatProperties.getAdditionalTldSkipPatterns());
		}
		if (tomcatProperties.getRedirectContextRoot() != null) {
			customizeRedirectContextRoot(factory, tomcatProperties.getRedirectContextRoot());
		}
		if (tomcatProperties.getUseRelativeRedirects() != null) {
			customizeUseRelativeRedirects(factory, tomcatProperties.getUseRelativeRedirects());
		}
	}

步骤

  1. SpringBoot根据导入的依赖情况,给容器添加式容器工厂【TomcatServletWebServerFactory】
  2. 容器中某个组件要创建对象就会触发后置处理器:WebServerFactoryCustomizerBeanPostProcessor,只要是WebServerFactory,后置就会触发
  3. 后置处理器,从容器中获取所有的WebServerFactoryCustomizer,并调用定制方法

4.5.4. 嵌入式Servlet容器启动原理

什么时候创建嵌入式Servlet容器工厂?什么时候获取嵌入式的Servlet容器并启动Tomcat

  1. SpringBoot应用启动 运行方法
  2. refreshContext(context);SpringBoot刷新IOC容器【创建IOC容器对象,并初始化容器,创建容器】;如果是web应用创建AnnotationConfigServletWebServerApplicationContext,否则默认创建:AnnotationConfigApplicationContext
  3. refreshContext(context);刷新刚才创建好的ioc容器;
	@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// Prepare this context for refreshing.
			prepareRefresh();

			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);

			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			}

			catch (BeansException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Exception encountered during context initialization - " +
							"cancelling refresh attempt: " + ex);
				}

				// Destroy already created singletons to avoid dangling resources.
				destroyBeans();

				// Reset 'active' flag.
				cancelRefresh(ex);

				// Propagate exception to caller.
				throw ex;
			}

			finally {
				// Reset common introspection caches in Spring's core, since we
				// might not ever need metadata for singleton beans anymore...
				resetCommonCaches();
			}
		}
	}
  1. onRefresh();web的ioc容器重写onRefresh()方法
  2. webioc容器会创建Servlet容器;createWebServer()
  3. 获取Servlet容器工厂:ServletWebServerFactory factory = getWebServerFactory();
    从ioc容器中获取ServletWebServerFactory组件String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);创建对象;同时触发后置处理器对象,
    后置处理器对象获取所有定制器来先定制Servlet容器;
  4. 使用容器工厂获取容器
    this.webServer = factory.getWebServer(getSelfInitializer());
  5. 嵌入式的Servlet容器创建对象并启动Servlet容器;
    先启动嵌入式的Servlet容器,再将ioc容器剩下没有创建出的对象获取
    IOC容器启动创建嵌入式的Servlet容器

4.6 使用外置Servlet容器

嵌入式Servlet容器:应用打成可执行的Jar
优点:简单,便携
缺点:默认不支持JSP,优化定制比较复杂(使用定制器[ServerProperties、自定义WebServerFactoryCustomizer],自己编写来编写Servlet容器工厂WebServerFactory)

外置的Servlet容器;外面安装Tomcat—应用是以war包的方式打包;

4.6.1 使用外置Servlet容器步骤
  1. 必须创建一个以打包war的项目;(利用IDEA创建好录目)
    录目

  2. 将嵌入式的Tomcat指定为provied;

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
 </dependency>
  1. 必须编写一个SpringBootServletInitializer的子类,并调用configure
public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        // 传入SpringBoot的主程序
        return application.sources(SpringBoot04WebJspApplication.class);
    }

}

  1. 启动服务器就可以使用了
4.6.2 容器启动原理
  • jar包:执行SpringBoot主类的main方法,启动ioc容器,创建嵌入式的Servlet的容器;
  • war包:启动服务器,服务器启动SpringBoot应用【SpringBootServletInitializer】,启动ioc容器
1.servlet 3.0 规则
  1. 服务器启动(web应用启动)会创建当前web应用里面每一个jar里面ServletContainerInitializer实例;
  2. ServletContainerInitializer的实现放在META-INF/services文件夹下,有一个名为javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名;
  3. 还可以使用@HandlesTyes,在应用启动的时候加载我们需要的类;
2.流程
  1. 启动Tomact
  2. E:\MavenRepository\org\springframework\spring-web\5.2.0.RELEASE\spring-web-5.2.0.RELEASE.jar!\META-INF\services\javax.servlet.ServletContainerInitializer:
    SpringWeb模块里面有这个文件:org.springframework.web.SpringServletContainerInitializer
  3. SpringServletContainerInitializer将@HandlesTypes标注的这个所有类都传入到onStartup方法中的Set<Class<?>>中,
    为这些WebApplicationInitializer类型创建实例;
  4. 每一个WebApplication都调用自己的onStartUp方法;
    WebApplicationInitializersh实现
  5. 相当于我们SpringBootServletInitializer的类会被创建对象,并执行onStartup方法
  6. SpringBootServletInitializer实例执行onStartUp的时候会createRootApplicationContext(servletContext);创建容器
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);
		// 调用config方法,子类重写了这个方法,将SpringBoot的主程序类传进来
		builder = configure(builder);
		builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
		// 使用builder创建一个Spring应用
		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));
		}
		// 启动
		return run(application);
	}
  1. 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);
			// 刷新ioc容器
			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应用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值