SpringBoot和web开发
1 简介
-
使用SpringBoot步骤:
- 1️⃣创建SpringBoot应用,选中我们需要的模块。
- 2️⃣SpringBoot已经默认将这些场景配置好了,只需要在配置文件指定少量配置就可以运行起来。
- 3️⃣自己编写业务逻辑代码。
-
自动配置原理:这个场景SpringBoot帮我们配置了什么?能不能修改?能修改哪些配置?能不能扩展?
xxxAutoConfiguration:帮我们在容器中自动配置组件。
xxxProperties:配置类封装配置文件的内容。
2 SpringBoot对静态资源的映射规则
- 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 {
@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {
//静态资源处理器
@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);
//所有**/favicon.ico
mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
faviconRequestHandler()));
return mapping;
}
@Bean
public ResourceHttpRequestHandler faviconRequestHandler() {
ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
requestHandler
.setLocations(this.resourceProperties.getFaviconLocations());
return requestHandler;
}
}
}
}
-
1️⃣所有
/webjars/**
,都去classpath:/META-INF/resources/webjars/
找静态资源。- webjars:以jar包的形式引入静态资源。
- 官网。
- 比如:浏览器发送
http://localhost:8080/webjars/jquery/3.5.1/jquery.js
请求,SpringBoot就会到classpath:/META-INF/resources/webjars/jquery/3.5.1/jquery.js
这个路径查找jquery.js
文件。
<dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.5.1</version> </dependency>
-
2️⃣
/**
访问当前项目的任何资源(静态资源的文件夹):"classpath:/META-INF/resources/" "classpath:/resources/" "classpath:/static/" "classpath:/public/" "/"
- 比如:浏览器发送
http://localhost:8080/asserts/css/bootstrap.min.css
请求,SpringBoot就会到静态资源文件夹中寻找/asserts/css/bootstrap.min.css
的路径去寻找bootstrap.min.css
文件。
- 比如:浏览器发送
-
3️⃣欢迎页,静态资源文件夹下的index.html页面,被
/**
映射。 -
比如:浏览器发送
http://localhost:8080
请求,默认情况下,会去静态资源文件夹下找index.html页面。 -
4️⃣所有的
**/favicon.ico
都是在静态资源文件夹下寻找。
3 模板引擎
3.1 简介
- JSP、Velocity、Freemarker、Thymeleaf……都是模板引擎。
- 模板引擎的原理:
3.2 Thymeleaf
3.2.1 Thymeleaf概述
- SpringBoot推荐Thymeleaf。
- Thymeleaf语法更简单,功能更强大。
3.2.2 Thymeleaf准备
- 导入thymeleaf相关jar包的Maven坐标:
<properties>
<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
<thymeleaf-layout-dialect.version>2.3.0</thymeleaf-layout-dialect.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 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");
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
//略
}
- 从ThymeleafProperties的源码可知:我们只要将HTML页面放在
classpath:/templates/
,Thymeleaf就能自动渲染。
3.2.3 Thymeleaf使用
- 导入thymeleaf的名称空间:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
- 使用thymeleaf语法:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div th:text="helloworld">哇,好(*^▽^*)</div>
</body>
</html>
3.2.4 Thymeleaf语法规则
文本
th:text
:改变当前元素里面的文本内容。th:任意html属性
:来替换原生属性的值。th:insert
、th:replace
:片段包含。th:each
:遍历。th:if
、th:unless
、th:case
:条件判断。th:object
、th:with
:声明变量。th:attr
、th:attrprepend
、th:attrappend
:任意属性修改。th:value
、th:href
、th:src
:修改指定属性默认值。th:fragment
:声明片段。th"remove
:移除。
表达式
Simple expressions:(表达式语法)
Variable Expressions: ${...}:获取变量值;OGNL;
1)、获取对象的属性、调用方法
2)、使用内置的基本对象:
#ctx : the context object.
#vars: the context variables.
#locale : the context locale.
#request : (only in Web Contexts) the HttpServletRequest object.
#response : (only in Web Contexts) the HttpServletResponse object.
#session : (only in Web Contexts) the HttpSession object.
#servletContext : (only in Web Contexts) the ServletContext object.
${session.foo}
3)、内置的一些工具对象:
#execInfo : information about the template being processed.
#messages : methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.
#uris : methods for escaping parts of URLs/URIs
#conversions : methods for executing the configured conversion service (if any).
#dates : methods for java.util.Date objects: formatting, component extraction, etc.
#calendars : analogous to #dates , but for java.util.Calendar objects.
#numbers : methods for formatting numeric objects.
#strings : methods for String objects: contains, startsWith, prepending/appending, etc.
#objects : methods for objects in general.
#bools : methods for boolean evaluation.
#arrays : methods for arrays.
#lists : methods for lists.
#sets : methods for sets.
#maps : methods for maps.
#aggregates : methods for creating aggregates on arrays or collections.
#ids : methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).
Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
补充:配合 th:object="${session.user}:
<div th:object="${session.user}">
<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>
Message Expressions: #{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL;
@{/order/process(execId=${execId},execType='FAST')}
Fragment Expressions: ~{...}:片段引用表达式
<div th:insert="~{commons :: main}">...</div>
Literals(字面量)
Text literals: 'one text' , 'Another one!' ,…
Number literals: 0 , 34 , 3.0 , 12.3 ,…
Boolean literals: true , false
Null literal: null
Literal tokens: one , sometext , main ,…
Text operations:(文本操作)
String concatenation: +
Literal substitutions: |The name is ${name}|
Arithmetic operations:(数学运算)
Binary operators: + , - , * , / , %
Minus sign (unary operator): -
Boolean operations:(布尔运算)
Binary operators: and , or
Boolean negation (unary operator): ! , not
Comparisons and equality:(比较运算)
Comparators: > , < , >= , <= ( gt , lt , ge , le )
Equality operators: == , != ( eq , ne )
Conditional operators:条件运算(三元运算符)
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
Special tokens:
No-Operation: _
4 SpringMVC自动配置
4.1 SpringMVC自动配置
-
SpringBoot自动配置好了SpringMVC,下面SpringBoot对SpringMVC的默认配置(
WebMvcAutoConfiguration
):-
1️⃣Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans。- 自动配置了ViewResolver(视图解析器,根据方法的返回值得到视图对象(View),视图对象决定如何渲染(转发?重定向?))。
ContentNegotiatingViewResolver
:组合所有的视图解析器。- 如何定制?我们可以自己给容器中添加一个视图解析器,自动将其组合进来。
-
2️⃣Support for serving static resources, including support for WebJars (see below)。
- 支持静态资源文件夹路径和webjars。
-
3️⃣Static
index.html
support.。- 首页访问。
-
4️⃣Custom
Favicon
support (see below)。- favicon.ico。
-
5️⃣自动注册了 of
Converter
,GenericConverter
,Formatter
beans。Converter
:转换器。Formatter
:格式化器。
@Bean @ConditionalOnProperty(prefix = "spring.mvc", name = "date-format") //在配置文件中配置日期格式化的规则 public Formatter<Date> dateFormatter() { return new DateFormatter(this.mvcProperties.getDateFormat()); //日期格式化组件 }
- 我们自己添加的格式化器、转换器只需要放在容器中即可。
@Override public void addFormatters(FormatterRegistry registry) { for (Converter<?, ?> converter : getBeansOfType(Converter.class)) { registry.addConverter(converter); } for (GenericConverter converter : getBeansOfType(GenericConverter.class)) { registry.addConverter(converter); } for (Formatter<?> formatter : getBeansOfType(Formatter.class)) { registry.addFormatter(formatter); } }
-
6️⃣Support for
HttpMessageConverters
(see below)。- HttpMessageConverter:SpringMVC用来转换Http的请求和响应的。比如JavaBean和JSON之间的转换。
HttpMessageConverters
是从容器中获取的,并且获取所有的HttpMessageConverter。- 自己给容器中添加HttpMessageConverter,只需要自己将组件注册到容器中。
-
7️⃣Automatic registration of
MessageCodesResolver
(see below)。- 定义错误代码生成规则。
-
8️⃣Automatic use of a
ConfigurableWebBindingInitializer
bean (see below)。- 我们可以配置一个ConfigurableWebBindingInitializer来替换默认的。
-
ConfigurableWebBindingInitializer的作用就是初始化所有的WebDataBinder,而WebDataBinder的作用就是请求数据-->JavaBean。
-
-
9️⃣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 typeWebMvcConfigurerAdapter
, but without@EnableWebMvc
. If you wish to provide custom instances ofRequestMappingHandlerMapping
,RequestMappingHandlerAdapter
orExceptionHandlerExceptionResolver
you can declare aWebMvcRegistrationsAdapter
instance providing such components.If you want to take complete control of Spring MVC, you can add your own
@Configuration
annotated with@EnableWebMvc
.
4.2 扩展SpringMVC
- 以前的springmvc.xml中配置拦截器等:
<mvc:view-controller path="/hello" view-name="success"/>
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/hello"/>
<bean></bean>
</mvc:interceptor>
</mvc:interceptors>
- 现在在SpringBoot中,编写一个配置类(用@Configuration注解标注的类),并且继承WebMvcConfigurerAdapter,但是
不要标注@EnableWebMvc注解
。这样既可以保留自动配置功能,又能使用我们自己扩展的配置。
package com.sunxiaping.springboot.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class SpringMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/index").setViewName("index");
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
registry.addViewController("/index.htm").setViewName("index");
}
}
-
原理:
- 1️⃣WebMvcAutoConfiguration是SpringMVC的自动配置类。
- 2️⃣在做其他自动配置的时候,会导入@Import(EnableWebMvcConfiguration.class)。
@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 { @Configuration @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter { //其他略 } @Configuration public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration { } //其他略 }
- 3️⃣DelegatingWebMvcConfiguration的部分源码如下:
@Configuration public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); //会将所有的WebMvcConfigurer都导入到容器中 @Autowired(required = false) public void setConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { this.configurers.addWebMvcConfigurers(configurers); } } //循环遍历所有的WebMvcConfigurer,并将每个WebMvcConfigurer配置的ViewController加入到容器中 //这样就可以将所有的WebMvcConfigurer的相关配置一起作用 @Override protected void addViewControllers(ViewControllerRegistry registry) { this.configurers.addViewControllers(registry); } //其他略 }
- 4️⃣容器中的所有WebMvcConfigurer都会一起起作用。
- 5️⃣我们自己配置类也会被调用。
4.3 全面接管SpringMVC
- SpringBoot对SpringMVC的自动配置不需要了,所有的都需要我们配置。
- 只需要在配置类中加
@EnableWebMvc
注解即可。
package com.sunxiaping.springboot.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration @EnableWebMvc public class SpringMvcConfig extends WebMvcConfigurerAdapter { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/index").setViewName("index"); registry.addViewController("/").setViewName("index"); registry.addViewController("/index.html").setViewName("index"); registry.addViewController("/index.htm").setViewName("index"); } }
-
原理如下:
- @EnableWebMvc注解源码如下:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(DelegatingWebMvcConfiguration.class) //会向容器中导入DelegatingWebMvcConfiguration组件 public @interface EnableWebMvc { }
- DelegatingWebMvcConfiguration的部分源码如下:
@Configuration public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { //其他略 }
- WebMvcAutoConfiguration的部分源码如下:
@Configuration @ConditionalOnWebApplication @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurerAdapter.class }) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) //只有在容器中没有WebMvcConfigurationSupport组件的时候,WebMvcAutoConfiguration才会起效,而@EnableWebMvc会将WebMvcConfigurationSupport组件导入到容器中 @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { //其他略 }
5 如何修改SpringBoot的默认配置
- 1️⃣SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置(@Bean、@Component),如果有,就使用用户自己配置的;如果没有,才自动配置;如果有些组件可以有多个(比如ViewResolver),就将用户配置的和自己默认的组合起来。
- 2️⃣在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置。
- 3️⃣在SpringBoot中会有非常多的xxxCustomizer帮助我们进行定制配置。
6 错误处理机制
6.1 SpringBoot错误的默认效果
- 如果是浏览器:返回一个默认的错误页面:
- 如果是其他客户端:返回一个默认的JSON数据。
-
原理:可以参照ErrorMvcAutoConfiguration(错误处理的自动配置)。
- ErrorMvcAutoConfiguration的部分源码如下:
@Configuration @ConditionalOnWebApplication @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) // Load before the main WebMvcAutoConfiguration so that the error View is available @AutoConfigureBefore(WebMvcAutoConfiguration.class) @EnableConfigurationProperties(ResourceProperties.class) public class ErrorMvcAutoConfiguration { //给容器中添加DefaultErrorAttributes组件 @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); } //给容器中添加BasicErrorController组件 @Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers); } //给容器中添加ErrorPageCustomizer组件 @Bean public ErrorPageCustomizer errorPageCustomizer() { return new ErrorPageCustomizer(this.serverProperties); } @Configuration static class DefaultErrorViewResolverConfiguration { private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, ResourceProperties resourceProperties) { this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; } //给容器中添加DefaultErrorViewResolver组件 @Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean public DefaultErrorViewResolver conventionErrorViewResolver() { return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties); } } //其他略 }
- ErrorPageCustomizer:
@Configuration @ConditionalOnWebApplication @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) // Load before the main WebMvcAutoConfiguration so that the error View is available @AutoConfigureBefore(WebMvcAutoConfiguration.class) @EnableConfigurationProperties(ResourceProperties.class) public class ErrorMvcAutoConfiguration { /** * {@link EmbeddedServletContainerCustomizer} that configures the container's error * pages. */ private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { private final ServerProperties properties; protected ErrorPageCustomizer(ServerProperties properties) { this.properties = properties; } // @Value("${error.path:/error}") //getPath() = private String path = "/error"; //系统出现错误以后会来到error请求进行处理。就如同在web.xml中配置错误页面规则 @Override public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix() + this.properties.getError().getPath()); errorPageRegistry.addErrorPages(errorPage); } @Override public int getOrder() { return 0; } } //其他略 }
- 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 = getStatus(request); //getErrorAttributes其实就是调用的DefaultErrorAttributes的getErrorAttributes方法,而DefaultErrorAttributes是从容器中获取的。 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 ? new ModelAndView("error", model) : modelAndView); } //产生JSON数据,其他客户端来到这个方法处理 @RequestMapping @ResponseBody 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<Map<String, Object>>(body, status); } //略 }
- DefaultErrorViewResolver:
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { private static final Map<Series, String> SERIES_VIEWS; static { Map<Series, String> views = new HashMap<Series, String>(); views.put(Series.CLIENT_ERROR, "4xx"); views.put(Series.SERVER_ERROR, "5xx"); SERIES_VIEWS = Collections.unmodifiableMap(views); } @Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { ModelAndView modelAndView = resolve(String.valueOf(status), 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); } private ModelAndView resolveResource(String viewName, Map<String, Object> model) { for (String location : this.resourceProperties.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); if (resource.exists()) { return new ModelAndView(new HtmlResourceView(resource), model); } } catch (Exception ex) { } } return null; } //其他略 }
- DefaultErrorAttributes:
@Order(Ordered.HIGHEST_PRECEDENCE) public class DefaultErrorAttributes{ //帮助我们在页面中共享信息 @Override public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>(); errorAttributes.put("timestamp", new Date()); addStatus(errorAttributes, requestAttributes); addErrorDetails(errorAttributes, requestAttributes, includeStackTrace); addPath(errorAttributes, requestAttributes); return errorAttributes; } }
-
步骤:
- 一旦系统出现4xx或者5xx之类的错误,ErrorPageCustomizer就会生效(定制错误的响应规则),就会来到/error请求,就会被BasicErrorController处理。
- 响应页面:去哪个页面是由DefaultErrorViewResolver解析得到的。
public abstract class AbstractErrorController implements ErrorController { /** * Resolve any specific error views. By default this method delegates to * {@link ErrorViewResolver ErrorViewResolvers}. * @param request the request * @param response the response * @param status the HTTP status * @param model the suggested model * @return a specific {@link ModelAndView} or {@code null} if the default should be * used * @since 1.4.0 */ protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { for (ErrorViewResolver resolver : this.errorViewResolvers) { ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null; } //其他略 }
6.2 如果定制错误响应
6.2.1 如何定制错误页面
- 1️⃣如果有模板引擎的情况下,默认请求error/状态码,并且返回error/状态码的视图地址,那么我们只需要将错误页面命名为错误状态码.html并放在模板引擎文件夹(默认是classpath:/template)里面的error文件夹下。
- 我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(比如同时存在404.html和4xx.html,那么当发生404的错误的时候,会优先寻找404.html,一旦匹配到了,就响应404.html)。
- 页面能获取的信息:
- timestamp:时间戳。
- status:状态码。
- error:错误提示。
- exception:异常对象。
- errors:JSR303错误校验的信息。
- path:错误路径。
- 2️⃣如果没有模板引擎(模板引擎找不多对应的错误页面),就去静态资源文件夹下寻找。
- 3️⃣以上都没有错误页面,就来到SpringBoot默认的错误提示页面。
6.2.2 如何定制错误的JSON数据
- 1️⃣自定义异常处理&返回定义JSON数据:
package com.sunxiaping.springboot.config;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
/**
* @ControllerAdvice标注的类:就是对控制器进行增强,在这个类中,可以使用@InitBinder、@InitBinder和@ModelAttribute,这些功能都可以被所有的控制器共享
*/
@ControllerAdvice
public class SelfExceptionHandler {
//浏览器客户端返回的都是JSON
@ExceptionHandler(value = SelfException.class)
@ResponseBody
public Map<String,Object> handleException(SelfException e){
Map<String,Object> map = new HashMap<>();
map.put("code",1);
map.put("msg",e.getMessage());
return map;
}
}
- 2️⃣转发到/error进行自适应响应效果处理:
package com.sunxiaping.springboot.config;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* @ControllerAdvice标注的类:就是对控制器进行增强,在这个类中,可以使用@InitBinder、@InitBinder和@ModelAttribute,这些功能都可以被所有的控制器共享
*/
@ControllerAdvice
public class SelfExceptionHandler {
@ExceptionHandler(value = SelfException.class)
public String handleException(HttpServletRequest request, HttpServletResponse response, SelfException e) {
Map<String, Object> map = new HashMap<>();
//传入我们自己的错误状态码,否则就不会进入定制的错误页面的解析流程
request.setAttribute("javax.servlet.error.status_code", 500);
map.put("code", 1);
map.put("msg", e.getMessage());
return "forward:/error";
}
}
-
3️⃣将我们的定制数据携带出去:
- 出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes得到的(是AbstractErrorController中规定的方法),所以我们可以编写一个ErrorController的实现类或AbstractErrorController的子类,放在容器中。
- 页面上能用的数据或者是JSON返回能用的数据都是通过errorAttributes.getErrorAttributes()方法得到的,容器中的DefaultErrorAttributes.getErrorAttributes()方法是默认进行数据处理的。
package com.sunxiaping.springboot.config; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; /** * @ControllerAdvice标注的类:就是对控制器进行增强,在这个类中,可以使用@InitBinder、@InitBinder和@ModelAttribute,这些功能都可以被所有的控制器共享 */ @ControllerAdvice public class SelfExceptionHandler { @ExceptionHandler(value = SelfException.class) public String handleException(HttpServletRequest request, HttpServletResponse response, SelfException e) { Map<String, Object> map = new HashMap<>(); //传入我们自己的错误状态码 request.setAttribute("javax.servlet.error.status_code", 500); map.put("code", 1); map.put("msg", e.getMessage()); request.setAttribute("ext", map); return "forward:/error"; } }
package com.sunxiaping.springboot.config; import org.springframework.boot.autoconfigure.web.DefaultErrorAttributes; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import java.util.Map; @Component public class SelfErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace); Object ext = requestAttributes.getAttribute("ext", RequestAttributes.SCOPE_REQUEST); map.put("ext", ext); return map; } }
7 配置嵌入式Servlet容器
7.1 如何定制和修改Servlet容器的相关配置
7.1.1 修改和server有关的配置(ServerProperties)
server:
port: 8081
context-path: /spring
tomcat:
uri-encoding: utf-8
# 通用的servlet容器配置
server:
xxx:
# Tomcat的设置
server:
tomcat:
xxx:
7.1.2 编写一个EmbeddedServletContainerCustomizer(嵌入式的Servlet容器的定制器)来修改Servlet有关配置
package com.sunxiaping.springboot.config;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class SpringMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/index").setViewName("index");
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
registry.addViewController("/index.htm").setViewName("index");
}
@Bean
public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer() {
return new EmbeddedServletContainerCustomizer() {
/**定制嵌入式的Servlet容器的相关规则
* @param container
*/
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.setPort(8081);
}
};
}
}
7.1.3 注册Servlet三大组件
package com.sunxiaping.springboot.servet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class SelfServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("hello servlet");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
package com.sunxiaping.springboot.config;
import com.sunxiaping.springboot.servet.SelfServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ServletConfig {
/**
* 注册Servlet,向容器中添加ServletRegistrationBean组件
* 注册Filter,向容器中添加FilterRegistrationBean组件
* 注册Listener,向容器中添加ServletListenerRegistrationBean组件
*
* @return
*/
@Bean
public ServletRegistrationBean servletRegistrationBean() {
return new ServletRegistrationBean(new SelfServlet(), "/hello");
}
}
7.2 SpringBoot支持其他的Servlet容器
- SpringBoot默认支持Tomcat、Jetty、Undertow。
- Tomcat(默认支持):
<dependency>
<!-- 引入web模块,默认支持Tomcat -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
- Undertow:
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
7.3 嵌入式Servlet容器自动配置原理
- EmbeddedServletContainerAutoConfiguration:嵌入式的Servlet容器自动配置
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication
@Import(BeanPostProcessorsRegistrar.class)
//BeanPostProcessorsRegistrar:给容器中导入一些组件。
//导入了EmbeddedServletContainerCustomizerBeanPostProcessor
//后置处理器:Bean初始化前后(创建完对象,还没赋值)执行初始化工作。
public class EmbeddedServletContainerAutoConfiguration {
@Configuration
@ConditionalOnClass({ Servlet.class, Tomcat.class }) //判断当前是否引入了Tomcat的依赖
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)//判断当前容器没有用户自己定义的EmbeddedServletContainerFactory:嵌入式的Servlet容器工厂;作用是创建嵌入式的Servlet容器
public static class EmbeddedTomcat {
@Bean
public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory();
}
}
/**
* Nested configuration if Jetty is being used.
*/
@Configuration
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
WebAppContext.class })
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedJetty {
@Bean
public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
return new JettyEmbeddedServletContainerFactory();
}
}
/**
* Nested configuration if Undertow is being used.
*/
@Configuration
@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedUndertow {
@Bean
public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
return new UndertowEmbeddedServletContainerFactory();
}
}
//略
}
- EmbeddedServletContainerFactory:嵌入式的Servlet容器工厂
public interface EmbeddedServletContainerFactory {
/**
* 获取的嵌入式的Servlet容器
*/
EmbeddedServletContainer getEmbeddedServletContainer(
ServletContextInitializer... initializers);
}
- EmbeddedServletContainer:嵌入式的Servlet容器。
public interface EmbeddedServletContainer {
/**
* Starts the embedded servlet container. Calling this method on an already started
* container has no effect.
* @throws EmbeddedServletContainerException if the container cannot be started
*/
void start() throws EmbeddedServletContainerException;
/**
* Stops the embedded servlet container. Calling this method on an already stopped
* container has no effect.
* @throws EmbeddedServletContainerException if the container cannot be stopped
*/
void stop() throws EmbeddedServletContainerException;
/**
* Return the port this server is listening on.
* @return the port (or -1 if none)
*/
int getPort();
}
- 以TomcatEmbeddedServletContainerFactory为例:
public class TomcatEmbeddedServletContainerFactory
extends AbstractEmbeddedServletContainerFactory implements ResourceLoaderAware {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(
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传入进去,返回一个嵌入式的Servlet容器,并且启动Tomcat服务器
return getTomcatEmbeddedServletContainer(tomcat);
}
protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
Tomcat tomcat) {
return new TomcatEmbeddedServletContainer(tomcat, getPort() >= 0);
}
public TomcatEmbeddedServletContainer(Tomcat tomcat, boolean autoStart) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
initialize();
}
private void initialize() throws EmbeddedServletContainerException {
TomcatEmbeddedServletContainer.logger
.info("Tomcat initialized with port(s): " + getPortsDescription(false));
synchronized (this.monitor) {
try {
addInstanceIdToEngineName();
try {
// Remove service connectors to that protocol binding doesn't happen
// yet
removeServiceConnectors();
// 启动Tomcat
this.tomcat.start();
// We can re-throw failure exception directly in the main thread
rethrowDeferredStartupExceptions();
Context context = findContext();
try {
ContextBindings.bindClassLoader(context, getNamingToken(context),
getClass().getClassLoader());
}
catch (NamingException ex) {
// Naming is not enabled. Continue
}
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown
startDaemonAwaitThread();
}
catch (Exception ex) {
containerCounter.decrementAndGet();
throw ex;
}
}
catch (Exception ex) {
throw new EmbeddedServletContainerException(
"Unable to start embedded Tomcat", ex);
}
}
}
//略
}
-
我们对嵌入式容器的配置修改是怎么修改的?
- 1️⃣ServerProperties。ServerProperties其实是EmbeddedServletContainerCustomizer的子类。
- 2️⃣EmbeddedServletContainerCustomizer:定制器帮我们修改了Servlet容器的配置。
-
容器中导入了EmbeddedServletContainerCustomizerBeanPostProcessor:
public class EmbeddedServletContainerCustomizerBeanPostProcessor
implements BeanPostProcessor, BeanFactoryAware {
//在初始化之前
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
//如果当前初始化的是一个ConfigurableEmbeddedServletContainer类型的组件
if (bean instanceof ConfigurableEmbeddedServletContainer) {
//EmbeddedServletContainerCustomizer定制器,调用每个定制器的customize方法,来给Servlet容器进行属性赋值
postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer) bean);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
private void postProcessBeforeInitialization(
ConfigurableEmbeddedServletContainer bean) {
for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) {
customizer.customize(bean);
}
}
private Collection<EmbeddedServletContainerCustomizer> getCustomizers() {
if (this.customizers == null) {
// Look up does not include the parent context
this.customizers = new ArrayList<EmbeddedServletContainerCustomizer>(
this.beanFactory
//从容器中获取所有EmbeddedServletContainerCustomizer类型的组件
//所以定制容器,可以给容器中配置EmbeddedServletContainerCustomizer类型的组件
.getBeansOfType(EmbeddedServletContainerCustomizer.class,
false, false)
.values());
Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
this.customizers = Collections.unmodifiableList(this.customizers);
}
return this.customizers;
}
//其他略
}
总结:
- 1️⃣SpringBoot根据导入的依赖情况,给容器中添加相应的EmbeddedServletContainerFactory(以TomcatEmbeddedServletContainerFactory为例)。
- 2️⃣容器中某个组件要创建对象就会惊动后置处理器(EmbeddedServletContainerCustomizerBeanPostProcessor),是因为EmbeddedServletContainerAutoConfiguration中使用@Import注解,导入了BeanPostProcessorsRegistrar,而BeanPostProcessorsRegistrar是一个ImportBeanDefinitionRegistrar,向容器中导入了EmbeddedServletContainerCustomizerBeanPostProcessor,简而言之,只要是嵌入式的Servlet容器工厂,后置处理器就工作。
- 3️⃣后置处理器,从容器中获取所有的EmbeddedServletContainerCustomizer,调用定制器的customize(ServerProperties其实就是EmbeddedServletContainerCustomizer)。
7.4 嵌入式Servlet容器启动原理
- 1️⃣SpringBoot应用启动运行run方法。
- 2️⃣refreshContext(context);SpringBoot刷新IOC容器(创建IOC容器,并初始化容器,创建容器中的每一个组件);如果是Web应用,创建的是AnnotationConfigEmbeddedWebApplicationContext;如果不是web应用,创建的是ConfigurableWebApplicationContext。
- 3️⃣refresh(context);刷新刚才创建好的web容器。
@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();
// 刷新容器
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
- 4️⃣onRefresh();web的IOC容器重写了onRefresh方法。
- 5️⃣web的IOC容器会创建嵌入式的Servlet容器。
public class EmbeddedWebApplicationContext extends GenericWebApplicationContext {
@Override
protected void onRefresh() {
super.onRefresh();
try {
//创建嵌入式的Servlet容器
createEmbeddedServletContainer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start embedded container",
ex);
}
}
private void createEmbeddedServletContainer() {
EmbeddedServletContainer localContainer = this.embeddedServletContainer;
ServletContext localServletContext = getServletContext();
if (localContainer == null && localServletContext == null) {
//获取嵌入式的Servlet容器工厂
//从IOC容器中获取EmbeddedServletContainerFactory组件。TomcatEmbeddedServletContainerFactory创建对象,后置处理器一看是这个对象,就获取所有的定制器来先定制Servlet容器的相关配置
EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
this.embeddedServletContainer = containerFactory
.getEmbeddedServletContainer(getSelfInitializer());
}
else if (localServletContext != null) {
try {
getSelfInitializer().onStartup(localServletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context",
ex);
}
}
initPropertySources();
}
//其他略
}
- 6️⃣获取嵌入式的Servlet容器工厂:
public class EmbeddedWebApplicationContext extends GenericWebApplicationContext {
@Override
protected void onRefresh() {
super.onRefresh();
try {
//创建嵌入式的Servlet容器
createEmbeddedServletContainer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start embedded container",
ex);
}
}
private void createEmbeddedServletContainer() {
EmbeddedServletContainer localContainer = this.embeddedServletContainer;
ServletContext localServletContext = getServletContext();
if (localContainer == null && localServletContext == null) {
//获取嵌入式的Servlet容器工厂
//从IOC容器中获取EmbeddedServletContainerFactory组件。TomcatEmbeddedServletContainerFactory创建对象,后置处理器一看是这个对象,就获取所有的定制器来先定制Servlet容器的相关配置
EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
this.embeddedServletContainer = containerFactory
.getEmbeddedServletContainer(getSelfInitializer());
}
else if (localServletContext != null) {
try {
getSelfInitializer().onStartup(localServletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context",
ex);
}
}
initPropertySources();
}
//其他略
}
- 7️⃣使用容器工厂获取嵌入式的Servlet容器:
this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(getSelfInitializer());
- 8️⃣嵌入式的Servlet容器创建对象并启动Servlet容器。