1. web准备
首先创建SpringBoot应用,选择我们需要的模块;
SpringBoot已经默认将这些web场景配置好了,只需要在配置文件中指定少量配置就可以运行;
web场景, SpringBoot帮我们配置了什么?能不能修改?能修改哪些配置?能不能扩展?
- WebMvcAutoConfiguration:帮我们给容器中自动配置web组件
- WebMvcProperties:封装配置文件的内容
最后自己编写业务代码即可.
2. SpringBoot对静态资源的映射规则
SpringBoot对静态资源的处理在Web组件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 {
// ....
// WebMvcAutoConfigurationAdapter
@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {
// ....
// addResourceHandlers
@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/**")) {
// 所有/webjars/**,都去classpath:/META-INF/resources/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(
// 当前项目类路径下的静态资源目录
/*"classpath:/META-INF/resources/",
*"classpath:/resources/",
*"classpath:/static/",
*"classpath:/public/"
*/
this.resourceProperties.getStaticLocations())
.setCachePeriod(cachePeriod));
}
}
}
}
2.1 访问/webjars/**
经过查看WebMvcAutoConfigurationAdapter#addResourceHandlers
源码, 访问所有的/webjars/**
,都会去类路径下的classpath:/META-INF/resources/webjars/
下面找资源;
https://www.webjars.org 提供了webjars的库, 我们来引入jQuery的依赖.
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>
引入之后的依赖效果:
启动应用后, 访问 http://localhost:8082/boot1/webjars/jquery/3.3.1/jquery.js
, 可以读取到jquery.js文件内容.
2.2 访问当前项目下的静态资源
一样从上面的源码中发现, 访问/**
会从当前项目静态资源目录下查找需要的静态资源文件, 项目的静态资源目录如下:
- classpath:/META-INF/resources/
- classpath:/resources/
- classpath:/static/
- classpath:/public/
WebMvcAutoConfigurationAdapter#addResourceHandlers 追踪源码分析
静态资源访问url匹配 staticPathPattern=/**
// WebMvcAutoConfigurationAdapter
@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {
// ....
// addResourceHandlers
@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/**")) {
// 所有/webjars/**,都去classpath:/META-INF/resources/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(
// 当前项目类路径下的静态资源目录
/*"classpath:/META-INF/resources/",
*"classpath:/resources/",
*"classpath:/static/",
*"classpath:/public/"
*/
this.resourceProperties.getStaticLocations())
.setCachePeriod(cachePeriod));
}
}
}
ResourceProperties中追踪this.resourceProperties.getStaticLocations()
的源码
// ResourceProperties可以设置和静态资源有关的参数
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties implements ResourceLoaderAware {
private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" };
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/", "classpath:/resources/",
"classpath:/static/", "classpath:/public/" };
private static final String[] RESOURCE_LOCATIONS;
static {
RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length
+ SERVLET_RESOURCE_LOCATIONS.length];
System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0,
SERVLET_RESOURCE_LOCATIONS.length);
// 将CLASSPATH_RESOURCE_LOCATIONS复制到RESOURCE_LOCATIONS
System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS,
SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length);
}
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/] plus context:/ (the root of the servlet context).
*/
private String[] staticLocations = RESOURCE_LOCATIONS;
// ....
// 指向上面的静态资源路径 RESOURCE_LOCATIONS
public String[] getStaticLocations() {
return this.staticLocations;
}
// 可以指定staticLocations
public void setStaticLocations(String[] staticLocations) {
this.staticLocations = appendSlashIfNecessary(staticLocations);
}
}
准备好静态资源
启动应用后, 访问 http://localhost:8082/boot1/a.png, http://localhost:8082/boot1/b , http://localhost:8082/boot1/c.png, 测试结果都能访问到对应的静态资源,会去上面的静态资源目录下去找。
2.3 欢迎页映射
欢迎页,静态资源文件夹下的所有index.html
页面;被\**
映射;
查看欢迎页映射的源码WebMvcAutoConfigurationAdapter#welcomePageHandlerMapping
// WebMvcAutoConfiguration#WebMvcAutoConfigurationAdapter
@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {
// ....
// welcomePageHandlerMapping
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(
ResourceProperties resourceProperties) {
// 查看下面的WebMvcAutoConfiguration#WelcomePageHandlerMapping
return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(),
// this.mvcProperties.getStaticPathPattern()=/**
this.mvcProperties.getStaticPathPattern());
}
}
// WebMvcAutoConfiguration#WelcomePageHandlerMapping
static final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping {
// ...
// 如果访问路径匹配/**, 系统自动生产handler(Controller)并设置view为index.html,并转发到首页index.html
private WelcomePageHandlerMapping(Resource welcomePage,
String staticPathPattern) {
if (welcomePage != null && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage);
ParameterizableViewController controller = new ParameterizableViewController();
controller.setViewName("forward:index.html");
setRootHandler(controller);
setOrder(0);
}
}
}
ResourceProperties#getWelcomePage
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties implements ResourceLoaderAware {
// ...
// getWelcomePage -> getStaticWelcomePageLocations 将/index.html封装到Resource对象
public Resource getWelcomePage() {
for (String location : getStaticWelcomePageLocations()) {
Resource resource = this.resourceLoader.getResource(location);
try {
if (resource.exists()) {
resource.getURL();
return resource;
}
}
catch (Exception ex) {
// Ignore
}
}
return null;
}
// 查找项目路径下的index.html, 查找链 /** -> resources/static,public,resources目录下的index.html
private String[] getStaticWelcomePageLocations() {
String[] result = new String[this.staticLocations.length];
for (int i = 0; i < result.length; i++) {
String location = this.staticLocations[i];
if (!location.endsWith("/")) {
location = location + "/";
}
result[i] = location + "index.html";
}
return result;
}
}
访问 http://localhost:8082/boot1/,也会去静态资源目录下面去找index.html
,先匹配上哪个展示哪个目录下的index页面。
2.4 图标映射
所有的**/favicon.ico
都是在静态资源文件下查找。
查看源码 WebMvcAutoConfigurationAdapter#FaviconConfiguration
// WebMvcAutoConfiguration#WebMvcAutoConfigurationAdapter
@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {
// ...
// WebMvcAutoConfigurationAdapter#FaviconConfiguration
@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;
}
// 查找favicon.ico的路径 -> resourceProperties.getFaviconLocations()
@Bean
public ResourceHttpRequestHandler faviconRequestHandler() {
ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
requestHandler
.setLocations(this.resourceProperties.getFaviconLocations());
return requestHandler;
}
}
}
ResourceProperties#getFaviconLocations
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties implements ResourceLoaderAware {
private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" };
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/", "classpath:/resources/",
"classpath:/static/", "classpath:/public/" };
private static final String[] RESOURCE_LOCATIONS;
static {
RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length
+ SERVLET_RESOURCE_LOCATIONS.length];
System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0,
SERVLET_RESOURCE_LOCATIONS.length);
System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS,
SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length);
}
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/] plus context:/ (the root of the servlet context).
*/
private String[] staticLocations = RESOURCE_LOCATIONS;
// ...
// favicon.ico也是去项目类路径的静态资源默认目录下去查找
List<Resource> getFaviconLocations() {
List<Resource> locations = new ArrayList<Resource>(
this.staticLocations.length + 1);
if (this.resourceLoader != null) {
// this.staticLocations=RESOURCE_LOCATIONS -> CLASSPATH_RESOURCE_LOCATIONS
for (String location : this.staticLocations) {
locations.add(this.resourceLoader.getResource(location));
}
}
locations.add(new ClassPathResource("/"));
return Collections.unmodifiableList(locations);
}
}
3. 模板引擎
JSP、Freemarker、Thymeleaf都是渲染界面的模板引擎技术. view + model :
SpringBoot推荐使用Thymeleaf,语法更简单,功能更强大。
3.1 引入thymeleaf
using-boot-starter 中有使用介绍, 引入thymeleaf的依赖.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
切换thymeleaf版本
<properties>
<java.version>1.8</java.version>
<thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
<!--布局功能的支持程序 thymeleaf3主程序 layout2以上版本-->
<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
</properties>
3.2 Thymeleaf使用&语法
把html页面放在classpath:/templates/
下,thymeleaf就能自动渲染。
@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");
// classpath:/templates/ 模板html页面放到目录下
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
// ....
/**
* 可以在全局配置文件中指定模板文件存放的路径和模板文件后缀
* spring.thymeleaf.prefix=...
* spring.thymeleaf.suffix=...
*/
private String prefix = DEFAULT_PREFIX;
private String suffix = DEFAULT_SUFFIX;
// ....
}
编写测试案例, 在classpath:/templates/
下添加success.html
页面
需要导入thymeleaf的名称空间 xmlns:th="http://www.thymeleaf.org"
th:text 改变当前元素里面的文本内容;
th: 任意html属性;来替换原生属性的值。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>hello</title>
</head>
<body>
<h1>success</h1><br/>
<!--th:text 设置div里面的文本内容-->
<div id="div01" class="myDiv" th:id="div1" th:class="mydiv" th:utext="${hello}">
这里是欢迎信息
</div>
<hr/>
<div th:text="${hello}"></div>
<div th:utext="${hello}"></div>
</body>
</html>
编写controller映射方法
@Controller
public class HelloController {
/**
* 查出一些数据在页面展示
* 映射到 /template/success.html
*
* @return
*/
@RequestMapping("/success")
public String success(Map<String, Object> map) {
//classpath:/templates/success.html
map.put("hello", "<h1>你好</h1>");
//map.put("users", Arrays.asList("zhangsan", "wangwu", "lisi"));
return "success";
}
}
启动应用,访问 http://localhost:8082/boot1/success ,成功找到success.html页面。
th:id
, th:class
替换了原来的属性值.
更多thymeleaf用法可以查看帮助文档
4. SpringMVC自动配置
包 org.springframework.boot.autoconfigure.web
下配置了web的所有自动化场景组件.
4.1 Auto-Configuration
[SpringMVC Auto-Configuration 官方文档]( Spring Boot Reference Guide ), 自动配置主要在下面几个方面:
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
The auto-configuration adds the following features on top of Spring’s defaults:
- Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans.- 自动配置了视图解析器ViewResolver, 根据方法的返回值得到视图对象View, 视图对象决定如何渲染, 可能转发或重定向等等;
- ContentNegotiatingViewResolver 组合所有的视图解析器;
- 自定义视图解析器, 只需要给容器中添加一个自定义的视图解析器, SpringMVC会自动将其整合进来;
- Support for serving static resources, including support for WebJars (see below).
- 支持静态资源文件夹路径, webjars的访问
- Automatic registration of
Converter
,GenericConverter
,Formatter
beans.- Convert 转换器, 可以实现类型转换
- Formatter 格式化器, 日期格式化等.
- Support for
HttpMessageConverters
(see below).- HttpMessageConvert 用来转换http请求和响应的;
- 自己给容器中添加HttpMessageConvert, 只需要将自定义组件注册到容器中(@Bean, @Component)
- Automatic registration of
MessageCodesResolver
(see below).- 定义错误代码生成规则
- Static
index.html
support. 支持静态首页的访问配置 - Custom
Favicon
support (see below). 支持自定义 favicon图标 - Automatic use of a
ConfigurableWebBindingInitializer
bean (see below).- 配置ConfigurableWebBindingInitializer可以替换默认的web初始化器, 使用自定义的.
4.2 扩展SpringMVC
编写一个JavaConfig类, 继承WebMvcConfigurerAdapter
, 从而来实现Web功能扩展开发.
注意: 不能在该配置类上标注
@EnableWebMvc
, 否则它会全面接管默认的MVC配置, 使用自定义的.
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
super.addViewControllers(registry);
// 浏览器发送/atguigu请求来到success页面
registry.addViewController("/atguigu").setViewName("success");
}
}
上面的配置对应到xml是这样的:
<mvc:view-controller path="/atguigu" view-name="success"/>
controller编写
@Controller
public class HelloController {
/**
* 查出一些数据在页面展示
* 映射到 /template/success.html
*
* @return
*/
@RequestMapping("/success")
public String success(Map<String, Object> map) {
//classpath:/templates/success.html
map.put("hello", "<h1>你好</h1>");
map.put("users", Arrays.asList("zhangsan", "wangwu", "lisi"));
return "success";
}
}
打开浏览器, 访问 http://localhost:8082/boot1/atguigu
, 会转发到success.html
界面.
4.3 MVC自动配置原理
WebMvcAutoConfiguration是SpringMVC的自动配置类, 在自动配置WebMvcAutoConfigurationAdapter
时会导入 EnableWebMvcConfiguration
, 包含我们的扩展配置, 容器中所有的WebMvcConfigurer都会起作用。
@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 {
// ...
// WebMvcAutoConfigurationAdapter
@Configuration
@Import(EnableWebMvcConfiguration.class) //
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {
// ...
}
// Configuration equivalent to {@code @EnableWebMvc}.
@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
// ...
//从DelegatingWebMvcConfiguration继承过来的配置,容器中所有的WebMvcConfigurer都会起作用
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
// 从容器中获取所有的WebMvcConfigurer
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
// 将所有的注册到容器的,包含自定义的viewController添加到WebMvcConfigurer
@Override
protected void addViewControllers(ViewControllerRegistry registry) {
this.configurers.addViewControllers(registry);
}
}
}
4.4 全面接管SpringMVC
如果SpringBoot不适用SpringMVC的默认自动配置,转而使用我们自定义的配置,只需要在配置类上添加@EnableWebMvc
即可, 它会导致所有的SpringMVC的自动配置都失效。
@EnableWebMvc // 全面接管SpringMVC的自动配置
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
// ...
}
为什么添加@EnableWebMvc
注解后,会导致所有的SpringMVC的自动配置都失效呢 ?
从源码分析EnableWebMvc
导入了DelegatingWebMvcConfiguration
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
DelegatingWebMvcConfiguration
对WebMvc各个场景的基本功能配置提供了支持。
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
// ....
}
SpringMvc的自动配置类WebMvcAutoConfiguration
只会在容器中没有WebMvcConfigurationSupport
组件时才会生效, 所以如果在自定义配置类上添加了@EnableWebMvc
注解, 就相当于给容器中注册了WebMvcConfigurationSupport
组件, 会导致mvc默认的自动配置类WebMvcAutoConfiguration
失效.
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
WebMvcConfigurerAdapter.class })
// 容器中没有WebMvcConfigurationSupport时, 自动配置类才会生效.
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@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. 访问指定的首页
映射器没有映射到指定url会去当前项目静态目录(/resources, /static, /public)下面去查找匹配index.html
如果映射器映射到指定url,会自动匹配映射逻辑指定的视图view。
@Controller
public class HelloController {
/**
* 映射到/template/login.html
*
* @return
*/
@RequestMapping(path = {"/", "/index", "/index.html"})
public String index() {
return "login";
}
}
浏览器访问 http://localhost:8082/boot1/
, http://localhost:8082/boot1/index
和 http://localhost:8082/boot1/index.html
, 映射器都会去静态资源目录下匹配指定的login.html
. 这是SpringMvc中映射器的实现效果.
我们也可以不使用默认的映射器实现上面的效果, 我们首先注释掉 HelloController#index(...)
@Controller
public class HelloController {
/**
* 映射到/template/login.html
*
* @return
*/
/* @RequestMapping(path = {"/", "/index", "/index.html"})
public String index() {
return "login";
}*/
}
而是使用自定义的映射器去给url指定访问的资源, 可以通过实现WebMvcConfigurerAdapter
来扩展SpringMvc的功能.
/**
* 使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
*/
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
/**
* 所有的WebMvcConfigurerAdapter组件都会一起生效
*
* @return
*/
// 添加自定义url映射视图关系
@Bean
public WebMvcConfigurerAdapter webMvcConfigurerAdapter() {
WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 映射到登录页面login.html
registry.addViewController("/").setViewName("login");
// 映射到登录页面login.html
registry.addViewController("/index.html").setViewName("login");
// 登录成功后的展示页面dashboard.html
registry.addViewController("/main.html").setViewName("dashboard");
}
};
return adapter;
}
}
重启应用后, 浏览器访问 http://localhost:8082/boot1/
, http://localhost:8082/boot1/index.html
; 可以看到实现了映射到指定资源.
7. 国际化
SpringBoot应用中实现国际化的操作步骤有:
- 编写国际化配置文件
- 使用ResourceBundleMessageSource管理国际化资源文件
- 在页面使用fmt:message取出国际化内容
编写国际化配置文件,抽取页面需要展示的国际化消息
SpringBoot自动配置好了管理国际化资源文件的组件 MessageSourceAutoConfiguration
@Configuration
@ConditionalOnMissingBean(value = MessageSource.class, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "spring.messages")
public class MessageSourceAutoConfiguration {
// 国际化配置文件可以直接放在类路径下, 定义为messages.properties, 系统默认识别并解析该文件
// 也可以在全局配置文件中使用spring.messages.basename修改国际化文件读取目录
private String basename = "messages";
}
修改国际化配置文件的读取基础名
# 配置国际化文件基础名
spring.messages.basename=international.login
去页面login.html
获取国际化信息的值 , 使用了thymeleaf模板引擎.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="#{login.tip}">登录页面</title>
</head>
<body>
<h1 th:text="#{login.tip}">登录页面</h1>
<div>
<form action="dashboard.html" th:action="@{/user/login}" method="post">
<span style="color: red" th:utext="${msg}"></span><br/>
<input type="text" name="username" placeholder="请输入用户名" th:placeholder="#{login.username}"/><br/>
<input type="password" name="password" placeholder="请输入密码" th:placeholder="#{login.password}"/><br/>
<input type="checkbox" value="记住密码"/>[[#{login.remember}]]<br/>
<input type="submit" value="登录" th:value="#{login.button}"/>
</form>
</div>
<div>
<a href="#">中文</a>
<a href="#">English</a>
</div>
</body>
</html>
SpringMvc默认根据当前请求头中携带的区域信息locale来进行配置国际化.
WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#localeResolver
源码分析:
@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 {
// ...
// WebMvcAutoConfigurationAdapter
@Configuration
@Import(EnableWebMvcConfiguration.class) //
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {
// ...
// LocaleResolver
@Bean
@ConditionalOnMissingBean
// 读取全局配置文件中的spring.mvc.locale区域信息进行国际化配置
// FIXED : Always use the configured locale.
// ACCEPT_HEADER: Use the "Accept-Language" header or the configured locale if the header is not set.
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
if (this.mvcProperties
.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
// 如果全局配置文件中的spring.mvc.locale没有指定, 就根据请求头中的AcceptHeaderLocale获取区域信息, 进行国际化配置
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
}
}
AcceptHeaderLocaleResolver中根据Accept-Language中的区域信息来配置国际化.
public class AcceptHeaderLocaleResolver implements LocaleResolver {
private Locale defaultLocale;
public void setDefaultLocale(Locale defaultLocale) {
this.defaultLocale = defaultLocale;
}
public Locale getDefaultLocale() {
return this.defaultLocale;
}
// resolveLocale
@Override
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = getDefaultLocale();
// 如果Accept-Language为空,就使用默认的国际化配置
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
}
Locale requestLocale = request.getLocale();
if (isSupportedLocale(requestLocale)) {
return requestLocale;
}
Locale supportedLocale = findSupportedLocale(request);
if (supportedLocale != null) {
return supportedLocale;
}
return (defaultLocale != null ? defaultLocale : requestLocale);
}
}
也可以在界面点击切换链接进行国际化切换,在切换链接后面指定国际化区域信息 /login.html?l=zh_CN
<div>
<a th:href="@{/index.html(l='zh_CN')}">中文</a>
<a th:href="@{/index.html(l='en_US')}">English</a>
</div>
在WebMvcConfigurerAdapter
中扩展自定义 LocaleResolver
/**
* 使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
*/
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
// ...
// 使用自定义的MyLocaleResolver
@Bean
public LocaleResolver localeResolver() {
return new MyLocaleResolver();
}
}
/**
* 可以在链接上携带区域信息
*/
public class MyLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 获取/login.html?l=zh_CN中的请求参数l
String l = request.getParameter("l");
Locale locale = null;
if (!StringUtils.isEmpty(l)) {
String[] sp = l.split("_"); // en_US zh_CN
locale = new Locale(sp[0], sp[1]); // language,country
} else {
// 这里必须默认初始化一个bean,如果将该国际化配置到springBoot中,第一次进入界面调用国际化组件没有参数l,返回null,会报空指针
locale = new Locale("zh", "CN");
}
return locale;
}
@Override
public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
}
}
重启应用后,访问 http://localhost:8082/boot1/index.html
, http://localhost:8082/boot1/index
点击【中文 English】链接切换国际化, 地址栏变化http://localhost:8082/boot1/index.html?l=en_US
开发期间thymeleaf模板引擎页面修改后要实时生效, 可以在全局配置文件禁用缓存
#禁用缓存 spring.thymeleaf.cache=false
8. RestfulCRUD
8.1 拦截器
登录提交的校验可以使用拦截器, 下面我们写一个拦截器, 来实现如果用户没登录就不允许访问/main.html
/**
* 登录检查
*/
public class LoginHandlerIntercepter implements HandlerInterceptor {
/**
* 目标处理器方法执行之前执行
*/
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
Object loginUser = httpServletRequest.getSession().getAttribute("loginUser");
System.out.println("拦截器登录校验 loginUser=" + loginUser);
if (loginUser == null) {
// 没有登录,返回登录页面
httpServletRequest.setAttribute("msg", "没有权限,请先登录");
httpServletRequest.getRequestDispatcher("/index.html").forward(httpServletRequest, httpServletResponse);
return false;
}
// 已经登录,放行
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 {
}
}
在配置类中扩展SpringMvc的功能, 增加拦截器.
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
/**
* 所有的WebMvcConfigurerAdapter组件都会一起生效
*
* @return
*/
// 添加自定义url映射视图关系
@Bean
public WebMvcConfigurerAdapter webMvcConfigurerAdapter() {
WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
registry.addViewController("/index.html").setViewName("login");
// /main.html请求会映射到dashboard.html界面
registry.addViewController("/main.html").setViewName("dashboard");
}
// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// /** 拦截所有请求进行登录验证,但是排除第一次进入登录页面的请求
registry.addInterceptor(new LoginHandlerIntercepter()).addPathPatterns("/**").excludePathPatterns("/index.html", "/user/login", "/");
}
};
return adapter;
}
}
login.html
界面, 如果接口返回了错误msg, 就展示错误信息.
<div>
<form action="dashboard.html" th:action="@{/user/login}" method="post">
<span style="color: red" th:utext="${msg}" th:if="${not #strings.isEmpty(msg)}"></span><br/>
<input type="text" name="username" placeholder="请输入用户名" th:placeholder="#{login.username}"/><br/>
<input type="password" name="password" placeholder="请输入密码" th:placeholder="#{login.password}"/><br/>
<input type="checkbox" value="记住密码"/>[[#{login.remember}]]<br/>
<input type="submit" value="登录" th:value="#{login.button}"/>
</form>
</div>
编写登录提交的接口controller
// 登录api
@Controller
public class LoginController {
@PostMapping(value = "/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Map<String, Object> map, HttpSession session) {
if (!StringUtils.isEmpty(username) && Objects.equals(CONSTANT.PASSWORD.getValue(), password)) {
// 登录成功,设置session
session.setAttribute("loginUser", username);
//登录成功 防止表单重复提交,可以重定向到主页
return "redirect:/main.html";
} else {
// 登录失败
map.put("msg", "用户名或密码错误");
return "login";
}
}
private enum CONSTANT {
PASSWORD("123456", "登录密码");
private String value;
private String msg;
CONSTANT(String value, String msg) {
this.value = value;
this.msg = msg;
}
public String getValue() {
return value;
}
public String getMsg() {
return msg;
}
}
}
重启应用, 浏览器访问http://localhost:8082/boot1/user/login
, 点击"登录"按钮提交, 拦截器中的登录校验生效, 出现接口返回的提示信息.
输入正确的用户,密码后, 重定向到 http://localhost:8082/boot1/main.html
, 映射到dashboard.html
界面.
dashboard.html
源码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>dashboard</title>
</head>
<body>
<h1>dashboard</h1><br/>
<h3>登录成功</h3>
<!--th:fragment抽取公共片段, 片段名tolist, id选择器tolist-->
<a th:href="@{/emps}" th:fragment="link" id="tolist">去到员工管理页面</a>
</body>
</html>
8.2 CRUD员工
8.2.1 实验要求
(1) RestfulCRUD: CRUD满足Rest风格
URI: /资源名称/资源标识 HTTP请求方式区分对资源CRUD操作.
function | 普通CRUD(uri来区分操作) | Restful CRUD |
---|---|---|
查询 | getEmp | emp—GET |
添加 | addEmp?xxx | emp—POST |
修改 | updateEmp?id=1&name=xxx&… | emp/{id}—PUT |
删除 | deleteEmp?id=1 | emp/{id}—DELETE |
(2) 实验的请求架构
实验功能 | 请求URI | 请求方式 |
---|---|---|
查询所有员工 | emps | GET |
查询某个员工(来到修改页面) | emp/1 | GET |
来到添加页面 | emp | GET |
添加员工 | emp | POST |
来到修改页面(查出员工信息回显) | emp/1 | GET |
修改员工 | emp | PUT |
删除员工 | emp/1 | DELETE |
8.2.2 员工列表
thymeleaf公共页面元素抽取
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>dashboard</title>
</head>
<body>
<h1>dashboard</h1><br/>
<h3>登录成功</h3>
<!--th:fragment抽取公共片段, 片段名tolist, id选择器tolist-->
<a th:href="@{/emps}" th:fragment="link" id="tolist">去到员工管理页面</a>
</body>
</html>
thymeleaf页面引入公共片段
<!--dashboard页面的link公共片段引入-->
<!--片段名方式引入 link-->
<div th:insert="~{dashboard::link}"></div>
<!--选择器方式引入 tolist-->
<div th:replace="~{dashboard::#tolist}"></div>
三种引入功能片段的属性
th:insert 将公共片段整个插入到声明引入的元素中
th:replace 将引入的元素替换为公共判断
th:include 将被引入的片段的内容包含进声明的标签中
员工列表页面 list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="员工信息列表">页面管理</title>
</head>
<body>
<span style="color: red" th:utext="${msg}"></span><br/>
<h1 th:text="员工列表"></h1>
<button><a th:href="@{/emp}">员工添加</a></button>
<table border="1">
<tr>
<td>id</td>
<td>lastName</td>
<td>email</td>
<td>gender</td>
<td>department</td>
<td>birthday</td>
<td>操作</td>
</tr>
<tr th:each="emp : ${emps}">
<td th:text="${emp.id}"></td>
<td th:text="${emp.lastName}"></td>
<td th:text="${emp.email}"></td>
<td>[[(${emp.gender}==0?'女':'男')]]</td>
<td>[[(${emp.department.departmentName})]]</td>
<td th:text="${#dates.format(emp.birthday,'yyyy/MM/dd HH:mm:ssSSS')}"></td>
<td>
<button><a th:href="@{/emp/}+${emp.id}">修改</a></button>
<!--<form th:action="@{/emp/}+${emp.id}" method="post">
<input type="hidden" name="_method" value="delete"/>
<button type="submit" style="color: blueviolet">删除</button>
</form>-->
<!--上面删除的方式,每行都会产生一个表单,我们用js事件改进, 自定义属性del_uri-->
<button style="color: blueviolet" class="deleteBtn" th:attr="del_uri=@{/emp/}+${emp.id}">删除</button>
</td>
</tr>
</table>
<!--dashboard页面的link公共片段引入-->
<!--片段名方式引入 link-->
<div th:insert="~{dashboard::link}"></div>
<!--选择器方式引入 tolist-->
<div th:replace="~{dashboard::#tolist}"></div>
<!--引入jQuery包-->
<script type="text/javascript" th:src="@{/webjars/jquery/3.3.1/jquery.js}"></script>
<!--创建删除按钮提交的form表单-->
<form id="deleteEmpForm" method="post">
<input type="hidden" name="_method" value="delete"/>
</form>
<!--给“删除”按钮绑定点击事件-->
<script>
$(".deleteBtn").click(function () {
// 删除当前员工
$("#deleteEmpForm").attr("action", $(this).attr("del_uri")).submit();
return false;
});
</script>
</body>
</html>
dashboard.html
页面点击【去到员工管理页面】链接,访问/emps
去到员工管理页面展示员工列表信息。
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
// 查询所有员工列表
@RequestMapping(value = "/emps", method = RequestMethod.GET)
public String list(Model model) {
Collection<Employee> employees = employeeDao.getAll();
// 放在请求域中进行共享
model.addAttribute("emps", employees);
// thymeleaf默认就会拼串儿,会拼接到类路径下 classpath:/templates/emp/list.html
return "emp/list";
}
}
准备访问数据。
// 准备数据
@Repository
public class EmployeeDao {
private static Map<Integer, Employee> employees = null;
@Autowired
private DepartmentDao departmentDao;
static {
employees = new HashMap<>();
employees.put(1001, new Employee(1001, "E-AA", "aa@163.com", 1, new Department(101, "D-AA"), new Date()));
employees.put(1002, new Employee(1002, "E-BB", "bb@163.com", 1, new Department(102, "D-BB"), new Date()));
employees.put(1003, new Employee(1003, "E-CC", "cc@163.com", 0, new Department(103, "D-CC"), new Date()));
employees.put(1004, new Employee(1004, "E-DD", "dd@163.com", 0, new Department(104, "D-DD"), new Date()));
employees.put(1005, new Employee(1005, "E-EE", "ee@163.com", 1, new Department(105, "D-EE"), new Date()));
}
private static Integer initId = 1006;
// 添加or修改
public void save(Employee employee) {
if (employee.getId() == null) {
employee.setId(initId++);
}
employee.setDepartment(departmentDao.getDepartment(employee.getDepartment().getId()));
employees.put(employee.getId(), employee);
}
public Collection<Employee> getAll() {
return employees.values();
}
public Employee get(Integer id) {
return employees.get(id);
}
public void delete(Integer id) {
employees.remove(id);
}
}
@Repository
public class DepartmentDao {
private static Map<Integer, Department> departments = null;
static {
departments = new HashMap<>();
departments.put(101, new Department(101, "D-AA"));
departments.put(102, new Department(102, "D-BB"));
departments.put(103, new Department(103, "D-CC"));
departments.put(104, new Department(104, "D-DD"));
departments.put(105, new Department(105, "D-EE"));
}
public Collection<Department> getDepartments() {
return departments.values();
}
public Department getDepartment(Integer id) {
return departments.get(id);
}
}
测试效果
8.2.3 添加员工
<button><a th:href="@{/emp}">员工添加</a></button>
点击【员工添加】按钮, 访问路径/emp
,get请求,跳转到员工添加的录入界面 add.html
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
@Autowired
DepartmentDao departmentDao;
// ...
// 来到员工添加页面
@GetMapping("/emp")
public String toAddPage(Model model) {
// 来到添加页面,查出所有的部门,在页面显示
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments", departments);
// thymeleaf默认就会拼串儿,会拼接到类路径下 classpath:/templates/emp/add.html
return "emp/add";
}
}
add.html
页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>员工添加页面</title>
</head>
<body>
<h1>员工添加</h1>
<form action="#" method="post" th:action="@{/emp}">
<table border="1">
<tr>
<td>lastName:</td>
<td><input type="text" name="lastName" placeholder="zhangsan"/></td>
</tr>
<tr>
<td>email:</td>
<td><input type="text" name="email" placeholder="zhangsan@aiguigu.com"/></td>
</tr>
<tr>
<td>gender:</td>
<td>
男 <input type="radio" name="gender" value="1"/>
女 <input type="radio" name="gender" value="0"/>
</td>
</tr>
<tr>
<td>department:</td>
<td>
<select name="department.id">
<option th:each="department:${departments}"
th:text="${department.id} +' '+ ${department.departmentName}"
th:value="${department.id}">
</option>
</select>
</td>
</tr>
<tr>
<td>birthday:</td>
<td><input type="text" name="birthday"/></td>
</tr>
<tfoot>
<tr>
<td colspan="2" align="center"><input type="submit" value="添加"/></td>
</tr>
</tfoot>
</table>
</form>
<div th:replace="~{dashboard::#tolist}"></div>
</body>
</html>
录入添加的员工信息
信息录入完后提交post请求到/emp
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
@Autowired
DepartmentDao departmentDao;
// ...
// 员工添加 SpringMVC自动将请求参数和pojo对象映射,只要参数名和pojo属性名称一致即可
@PostMapping("/emp")
public String addEmp(Employee employee) {
employeeDao.save(employee);
System.out.println("add emp finished");
return "redirect:/emps";
}
}
提交发现报错
原因:界面birthday映射到后台employee对象的birthday字段是日期类型,SpringBoot对SpringMVC自动配置的日期类型是yyyy/MM/dd,而我们填写的birthday是yyyyMMdd,可以修改映射配置即可。
# 日期映射格式
spring.mvc.date-format=yyyyMMdd
重新启动添加员工成功, 重定向到员工列表页面,可以看到刚添加的那条记录。
8.2.4 修改员工
员工列表页面新增“修改”按钮。
<td>
<button><a th:href="@{/emp/}+${emp.id}">修改</a></button>
</td>
点击【修改】按钮,提交get请求到/emp/{id}
, 查询要修改的员工信息回显到修改页面edit.html
。
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
@Autowired
DepartmentDao departmentDao;
// ...
// 来到修改页面,查出当前员工,在修改页面回显
@GetMapping("/emp/{id}")
public String toEditPage(@PathVariable("id") Integer id, Model model) {
Employee employee = employeeDao.get(id);
model.addAttribute("employee", employee);
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments", departments);
return "emp/edit";
}
}
修改界面edit.html
, SpringMVC中配置 HiddenHttpMethodFilter
通过_method属性
将post请求转换为put请求。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>员工修改界面</title>
</head>
<body>
<h1>员工修改</h1>
<form action="#" method="post" th:action="@{/emp}">
<!-- 发送put请求修改员工数据
1、SpringMVC中配置 HiddenHttpMethodFilter (SpringBoot自动配置好了)
2、页面创建一个post表单
3、创建一个input项,name="_method" ;值就是我们指定的请求方式
-->
<input type="hidden" name="_method" value="put" th:if="${employee!=null}"/>
<input type="hidden" name="id" th:value="${employee.id}" th:if="${employee!=null}"/>
<table border="1">
<tr>
<td>lastName:</td>
<td><input type="text" name="lastName" th:value="${employee.lastName}"/></td>
</tr>
<tr>
<td>email:</td>
<td><input type="text" name="email" th:value="${employee.email}"/></td>
</tr>
<tr>
<td>gender:</td>
<td>
男 <input type="radio" name="gender" value="1" th:checked="${employee.gender==1}"/>
女 <input type="radio" name="gender" value="0" th:checked="${employee.gender==0}"/>
</td>
</tr>
<tr>
<td>department:</td>
<td>
<select name="department.id">
<option th:each="department:${departments}"
th:text="${department.id} +' '+ ${department.departmentName}"
th:value="${department.id}"
th:selected="${department.id==employee.department.id}"/>
</select>
</td>
</tr>
<tr>
<td>birthday:</td>
<td><input type="text" name="birthday"
th:value="${#dates.format(employee.birthday,'yyyy-MM-dd')}"/></td>
</tr>
<tfoot>
<tr>
<td colspan="2" align="center"><input type="submit" value="修改"/></td>
</tr>
</tfoot>
</table>
</form>
<div th:replace="~{dashboard::#tolist}"></div>
</body>
</html>
修改员工信息后,提到put请求到/emp
进行修改操作。
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
@Autowired
DepartmentDao departmentDao;
// ...
// 修改员工信息
@PutMapping("/emp")
public String edit(Employee employee, @RequestParam("_method") String method) {
employeeDao.save(employee);
return "redirect:/emps";
}
}
修改员工信息后, 重定向到员工列表界面,验证刚刚修改的记录。
8.2.5 删除员工
员工列表页面新增“删除”按钮。
删除提交的是delete请求,form表单支持的是get或post请求,需要使用SpringMVC的过滤器HiddenHttpMethodFilter
配置转换成delete请求。
<td>
<button><a th:href="@{/emp/}+${emp.id}">修改</a></button>
<form th:action="@{/emp/}+${emp.id}" method="post">
<input type="hidden" name="_method" value="delete"/>
<button type="submit" style="color: blueviolet">删除</button>
</form>
</td>
点击“删除”按钮,提交delete请求到/emp/{id}
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
@Autowired
DepartmentDao departmentDao;
// ...
// 删除员工
@DeleteMapping("/emp/{id}")
public String delete(@PathVariable("id") Integer id) {
employeeDao.delete(id);
return "redirect:/emps";
}
}
上面删除按钮的提交实现性能不好,列表的每一行员工信息的删除操作都会产生一个form表单。可以使用js
事件优化。给“删除”按钮添加类选择器 class=“deleteBtn”,并使用thymeleaf设置自定义属性(提交的uri).
-
使用jquery的api,先引入webjars的jquery包。
-
创建删除按钮提交的form表单,form表单创建在列表外面且只有此一个。
-
给“删除”按钮绑定点击事件。
<td>
<button><a th:href="@{/emp/}+${emp.id}">修改</a></button>
<!--<form th:action="@{/emp/}+${emp.id}" method="post">
<input type="hidden" name="_method" value="delete"/>
<button type="submit" style="color: blueviolet">删除</button>
</form>-->
<!--上面删除的方式,每行都会产生一个表单,我们用js事件改进, 自定义属性del_uri-->
<button style="color: blueviolet" class="deleteBtn" th:attr="del_uri=@{/emp/}+${emp.id}">删除</button>
</td>
<!--引入jQuery包-->
<script type="text/javascript" th:src="@{/webjars/jquery/3.3.1/jquery.js}"></script>
<!--创建删除按钮提交的form表单-->
<form id="deleteEmpForm" method="post">
<input type="hidden" name="_method" value="delete"/>
</form>
<!--给“删除”按钮绑定点击事件-->
<script>
$(".deleteBtn").click(function () {
// 删除当前员工
$("#deleteEmpForm").attr("action", $(this).attr("del_uri")).submit();
return false;
});
</script>
8.2.6 HiddenHttpMethodFilter
上面的修改和删除中,使用HiddenHttpMethodFilter
读取_method
参数都将form表单的post请求方式成功转换到put和delete请求方式。源码分析如下:
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
/** Default method parameter: {@code _method} */
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = DEFAULT_METHOD_PARAM;
/**
* Set the parameter name to look for HTTP methods.
* @see #DEFAULT_METHOD_PARAM
*/
public void setMethodParam(String methodParam) {
Assert.hasText(methodParam, "'methodParam' must not be empty");
this.methodParam = methodParam;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request;
// 如果表单提交的是post请求,进入转换逻辑
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
// 读取_method参数的值
String paramValue = request.getParameter(this.methodParam);
// 判断_method值非空
if (StringUtils.hasLength(paramValue)) {
// 将_method值替换到HttpServletRequest中,实现修改http请求方式的效果
requestToUse = new HttpMethodRequestWrapper(request, paramValue);
}
}
// 过滤器继续执行,HttpServletRequest替换成了包装过的requestToUse
filterChain.doFilter(requestToUse, response);
}
/**
* Simple {@link HttpServletRequest} wrapper that returns the supplied method for
* {@link HttpServletRequest#getMethod()}.
*/
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
// 初始化将外部传入的_method值给到HttpServletRequest#method
this.method = method.toUpperCase(Locale.ENGLISH);
}
// HttpMethodRequestWrapper父类实现了HttpServletRequest,重写getMethod方法, 最终映射handler读取的HTTP请求方式就是转换包装后的_method
@Override
public String getMethod() {
return this.method;
}
}
}
public class HttpServletRequestWrapper extends ServletRequestWrapper implements
HttpServletRequest {
// ...
private HttpServletRequest _getHttpServletRequest() {
return (HttpServletRequest) super.getRequest();
}
/**
* The default behavior of this method is to return getMethod() on the
* wrapped request object.
*/
@Override
public String getMethod() {
return this._getHttpServletRequest().getMethod();
}
}
9. 错误处理机制
9.1 SpringBoot默认的错误处理机制
浏览器效果, 返回一个默认的错误页面
浏览器端发送请求的请求头中Accept中有text/html
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,
image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
其他客户端效果(postman等), 默认响应一个json数据.
{
"timestamp": 1682344395720,
"status": 500,
"error": "Internal Server Error",
"exception": "org.thymeleaf.exceptions.TemplateInputException",
"message": "An error happened during template parsing (template: \"class path resource [templates/emp/edit.html]\")",
"path": "/boot1/emp/1"
}
9.2 原理
可以参考ErrorMvcAutoConfiguration
,错误处理的自动配置类。它给容器中添加了以下组件.
ErrorPageCustomizer, BasicErrorController ,DefaultErrorViewResolver , DefaultErrorAttributes
9.2.1 异常解析过程
一旦系统出现4xx或5xx
的错误码,ErrorPageCustomizer
就会生效, 使用ErrorPageRegistry
将ErrorPage
注册到容器中, 映射到/error
请求;/error
请求会被BasicErrorController
处理,其提供了html
和json
响应类型; 如果响应类型是响应页面,去到哪个页面是由DefaultErrorViewResolver
解析得到的。
ErrorMvcAutoConfiguration源码, 查看注册的异常处理核心组件.
@Configuration
@ConditionalOnWebApplication // web应用才会生效,也就是要读到mvc环境
// Servlet和前端控制器存在, 该自动配置类才会生效
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
// 在WebMvcAutoConfiguration加载之前加载
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
// 绑定配置属性类ResourceProperties
@EnableConfigurationProperties(ResourceProperties.class)
public class ErrorMvcAutoConfiguration {
private final ServerProperties serverProperties;
private final List<ErrorViewResolver> errorViewResolvers;
public ErrorMvcAutoConfiguration(ServerProperties serverProperties,
ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
this.serverProperties = serverProperties;
this.errorViewResolvers = errorViewResolversProvider.getIfAvailable();
}
// 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);
}
}
}
9.2.2 ErrorPageCustomizer
系统出现错误以后来到error请求进行处理, 类似web.xml注册的错误页面规则, 将配置的error/xxx.html
异常处理界面注册到容器中.
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
protected ErrorPageCustomizer(ServerProperties properties) {
this.properties = properties;
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
// getServletPrefix 获取servlet匹配的前缀规则
ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()
+ this.properties.getError().getPath());
errorPageRegistry.addErrorPages(errorPage);
}
@Override
public int getOrder() {
return 0;
}
}
// ServerProperties#getServletPrefix 获取servlet匹配的前缀规则 /*/error
public String getServletPrefix() {
String result = this.servletPath;
if (result.contains("*")) {
result = result.substring(0, result.indexOf("*"));
}
if (result.endsWith("/")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
// ErrorProperties#getPath 获取系统出现异常后,错误处理的url: /error
public class ErrorProperties {
/**
* Path of the error controller.
*/
@Value("${error.path:/error}")
private String path = "/error";
public String getPath() {
return this.path;
}
}
9.2.3 BasicErrorController
当系统出现错误时, springboot会将去寻找处理错误的url (/error), 然后使用/error
去找到mvc中映射的handler处理错误.
// 处理错误的handler,默认路由到 /error 来处理
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
// AbstractErrorController#errorAttributes
private final ErrorAttributes errorAttributes;
// Create a new {@link BasicErrorController} instance.
public BasicErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
Assert.notNull(errorProperties, "ErrorProperties must not be null");
this.errorProperties = errorProperties;
}
@Override
public String getErrorPath() {
return this.errorProperties.getPath();
}
// 如果请求头中的Accept=text/html, 会在该方法中处理, 一般处理来自浏览器端的请求
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
// 获取http-status
HttpStatus status = getStatus(request);
// 将异常信息封装到model中
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
// 设置http请求状态
response.setStatus(status.value());
// 解析model数据并返回view
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
// 其他客户端发送的请求,出现异常时使用该方法处理并产生Json类型的响应数据
@RequestMapping
@ResponseBody // response 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<Map<String, Object>>(body, status);
}
// Provide access to the error properties.
protected ErrorProperties getErrorProperties() {
return this.errorProperties;
}
// 从请求参数中获取请求状态 javax.servlet.error.status_code
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
// 500
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
// 500
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
// 获取request中的请求参数
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
// DefaultErrorAttributes#getErrorAttributes
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}
}
9.2.4 DefaultErrorViewResolver
BasicErrorController#errorHtml
中使用到了resolveErrorView进行错误处理的视图解析.
// 继承自AbstractErrorController的处理异常的视图解析器
private final List<ErrorViewResolver> errorViewResolvers;
// BasicErrorController#resolveErrorView
protected ModelAndView resolveErrorView(HttpServletRequest request,
HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
// ErrorViewResolver#resolveErrorView 视图解析
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
DefaultErrorViewResolver#resolveErrorView
/**
* Default {@link ErrorViewResolver} implementation that attempts to resolve error views
* using well known conventions. Will search for templates and static assets under
* {@code '/error'} using the {@link HttpStatus status code} and the
* {@link HttpStatus#series() status series}.
* <p>
* For example, an {@code HTTP 404} will search (in the specific order):
* <ul>
* <li>{@code '/<templates>/error/404.<ext>'}</li>
* <li>{@code '/<static>/error/404.html'}</li>
* <li>{@code '/<templates>/error/4xx.<ext>'}</li>
* <li>{@code '/<static>/error/4xx.html'}</li>
* </ul>
*/
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new HashMap<Series, String>();
// 按照请求状态码进行匹配 /error/4xx.html, /error/5xx.html
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
// ...
/**
* Create a new {@link DefaultErrorViewResolver} instance.
* @param applicationContext the source application context
* @param resourceProperties resource properties
*/
@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) {
// viewName是请求状态码, 拼接后/error/400.html
String errorViewName = "error/" + viewName;
// 模板引擎可以解析这个页面地址的话, 就用这个页面地址封装视图
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
// TemplateAvailabilityProviders#getProvider
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
// 模板引擎找不到对应的页面,就去类路径下去找
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
/**
* /templates/error/404.html,4xx.html
* /static/error/404.html,4xx.html
* /public/error/404.html,4xx.html
* /resources/error/404.html,4xx.html
*/
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
// 如果找到自定义的错误处理的页面,封装View
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
/**
* {@link View} backed by an HTML resource.
*/
private static class HtmlResourceView implements View {
private Resource resource;
HtmlResourceView(Resource resource) {
this.resource = resource;
}
@Override
public String getContentType() {
return MediaType.TEXT_HTML_VALUE;
}
// 视图渲染
@Override
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.setContentType(getContentType());
FileCopyUtils.copy(this.resource.getInputStream(),
response.getOutputStream());
}
}
}
TemplateAvailabilityProviders#getProvider
public class TemplateAvailabilityProviders {
// 配置的可用模板
private final List<TemplateAvailabilityProvider> providers;
public TemplateAvailabilityProviders(ClassLoader classLoader) {
Assert.notNull(classLoader, "ClassLoader must not be null");
// 从spring.factories中加载Template availability providers
//FreeMarkerTemplateAvailabilityProvider,ThymeleafTemplateAvailabilityProvider...
this.providers = SpringFactoriesLoader
.loadFactories(TemplateAvailabilityProvider.class, classLoader);
}
public TemplateAvailabilityProvider getProvider(String view, Environment environment,
ClassLoader classLoader, ResourceLoader resourceLoader) {
// ....
RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(
environment, "spring.template.provider.");
if (!propertyResolver.getProperty("cache", Boolean.class, true)) {
// findProvider 匹配模板
return findProvider(view, environment, classLoader, resourceLoader);
}
TemplateAvailabilityProvider provider = this.resolved.get(view);
if (provider == null) {
synchronized (this.cache) {
provider = findProvider(view, environment, classLoader, resourceLoader);
provider = (provider == null ? NONE : provider);
this.resolved.put(view, provider);
this.cache.put(view, provider);
}
}
return (provider == NONE ? null : provider);
}
private TemplateAvailabilityProvider findProvider(String view,
Environment environment, ClassLoader classLoader,
ResourceLoader resourceLoader) {
for (TemplateAvailabilityProvider candidate : this.providers) {
// 从候选模板中匹配可用的模板页面
if (candidate.isTemplateAvailable(view, environment, classLoader,
resourceLoader)) {
return candidate;
}
}
return null;
}
}
ThymeleafTemplateAvailabilityProvider#isTemplateAvailable
public class ThymeleafTemplateAvailabilityProvider
implements TemplateAvailabilityProvider {
@Override
public boolean isTemplateAvailable(String view, Environment environment,
ClassLoader classLoader, ResourceLoader resourceLoader) {
if (ClassUtils.isPresent("org.thymeleaf.spring4.SpringTemplateEngine",
classLoader)) {
PropertyResolver resolver = new RelaxedPropertyResolver(environment,
"spring.thymeleaf.");
String prefix = resolver.getProperty("prefix",
ThymeleafProperties.DEFAULT_PREFIX); // classpath:/templates/
String suffix = resolver.getProperty("suffix",
ThymeleafProperties.DEFAULT_SUFFIX); // .html
// classpath:/templates/404.html
return resourceLoader.getResource(prefix + view + suffix).exists();
}
return false;
}
}
9.2.5 DefaultErrorAttributes
BasicErrorController解析浏览器端的请求或其他客户端请求的errorHtml
和error
的处理中都有获取异常相关信息的操作, 是从DefaultErrorAttributes中获取.
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes
implements ErrorAttributes, HandlerExceptionResolver, Ordered {
// ....
// 获取异常信息
@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);
// 存入请求uri
addPath(errorAttributes, requestAttributes);
return errorAttributes;
}
private void addStatus(Map<String, Object> errorAttributes,
RequestAttributes requestAttributes) {
// 获取异常的响应码javax.servlet.error.status_code的值
Integer status = getAttribute(requestAttributes,
"javax.servlet.error.status_code");
if (status == null) {
errorAttributes.put("status", 999);
errorAttributes.put("error", "None");
return;
}
errorAttributes.put("status", status);
try {
errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
}
catch (Exception ex) {
// Unable to obtain a reason
errorAttributes.put("error", "Http Status " + status);
}
}
private void addErrorDetails(Map<String, Object> errorAttributes,
RequestAttributes requestAttributes, boolean includeStackTrace) {
Throwable error = getError(requestAttributes);
if (error != null) {
while (error instanceof ServletException && error.getCause() != null) {
error = ((ServletException) error).getCause();
}
errorAttributes.put("exception", error.getClass().getName());
addErrorMessage(errorAttributes, error);
if (includeStackTrace) {
addStackTrace(errorAttributes, error);
}
}
// 获取异常信息javax.servlet.error.message的值
Object message = getAttribute(requestAttributes, "javax.servlet.error.message");
if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null)
&& !(error instanceof BindingResult)) {
// 存入异常信息message
errorAttributes.put("message",
StringUtils.isEmpty(message) ? "No message available" : message);
}
}
private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) {
BindingResult result = extractBindingResult(error);
if (result == null) {
errorAttributes.put("message", error.getMessage());
return;
}
if (result.getErrorCount() > 0) {
errorAttributes.put("errors", result.getAllErrors());
errorAttributes.put("message",
"Validation failed for object='" + result.getObjectName()
+ "'. Error count: " + result.getErrorCount());
}
else {
errorAttributes.put("message", "No errors");
}
}
private void addPath(Map<String, Object> errorAttributes,
RequestAttributes requestAttributes) {
// 获取请求uri
String path = getAttribute(requestAttributes, "javax.servlet.error.request_uri");
if (path != null) {
errorAttributes.put("path", path);
}
}
@Override
public Throwable getError(RequestAttributes requestAttributes) {
Throwable exception = getAttribute(requestAttributes, ERROR_ATTRIBUTE);
if (exception == null) {
exception = getAttribute(requestAttributes, "javax.servlet.error.exception");
}
return exception;
}
@SuppressWarnings("unchecked")
private <T> T getAttribute(RequestAttributes requestAttributes, String name) {
return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
}
}
9.3 定制异常响应页面
9.3.1 有模板引擎的场景
将错误页面命名为 错误状态码.html
放在模板引擎下的error
文件夹下,发生此状态码的错误就会来到对应的页面。error/状态码.html;
我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确匹配优先(优先寻找精确的errorCode.html页面)。
// DefaultErrorViewResolver#SERIES_VIEWS, 保存请求出现错误时的默认处理页面前缀4xx 5xx
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);
}
// DefaultErrorViewResolver#resolveErrorView
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
// 优先精确匹配, 错误响应状态码 404 ,500
ModelAndView modelAndView = resolve(String.valueOf(status), model);
// 如果精确匹配失败,就按照默认规则找4xx, 5xx
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
// status.series()就是用错误响应状态码/100,返回匹配上的枚举, 查看HttpStatus.Series#valueOf
// 然后再从SERIES_VIEWS中获取是4xx还是5xx页面前缀
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
// viewName就是精确匹配的错误响应状态码或默认匹配的4xx,5xx
String errorViewName = "error/" + viewName; // error/4xx
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
// 模板解析完成后, 得到完整的异常处理页面路径 error/4xx.html
return new ModelAndView(errorViewName, model);
}
// 如果模板引擎找不到, 就去"classpath:/META-INF/resources/", "classpath:/resources/",
// "classpath:/static/", "classpath:/public/" 去找 error/4xx.html
return resolveResource(errorViewName, model);
}
DefaultErrorAttributes使得页面能获取的信息:
-
timestamp 时间戳
-
status 状态码
-
error 错误提示
-
exception 异常
-
message 异常消息
-
path 请求url
/error/4xx.html
页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>4xx异常</title>
</head>
<body>
<h5 th:text="4xx异常">系统异常</h5>
<h5>time:[[(${#dates.format(timestamp,'yyyy-MM-dd HH:mm:ss')})]]</h5>
<h5>status:[[(${status})]]</h5>
<h5>error:[[(${error})]]</h5>
<h5>exception:[[(${exception})]]</h5>
<h5>message:[[(${message})]]</h5>
<h5>errors:[[(${errors})]]</h5>
<h5>trace:[[(${trace})]]</h5>
<h5>path:[[(${path})]]</h5>
</body>
</html>
启动应用后, 访问一个不存在的请求url, 如果没有提供精确匹配的404.html页面, 会继续匹配系统默认的4xx.html界面渲染异常信息.
如果提供了404.html页面, 就会优先精确匹配的404.html页面来渲染异常信息.
9.3.2 无模板引擎的场景
模板引擎找不到这个错误页面,就去静态资源文件夹下找。
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
"/" 当前项目的根路径
以上都没有错误页面,就来到SpringBoot默认的错误页面渲染异常信息。
具体源码在ErrorMvcAutoConfiguration中:
@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
// 创建默认的异常渲染界面
private final SpelView defaultErrorView = new SpelView(
"<html><body><h1>Whitelabel Error Page</h1>"
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
+ "<div id='created'>${timestamp}</div>"
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
+ "<div>${message}</div></body></html>");
// 异常渲染的viewBean
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
@Bean
@ConditionalOnMissingBean(BeanNameViewResolver.class)
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
9.4 定制异常响应Json
9.4.1 自定义异常处理
自定义用户不存在的异常类
public class UserNotExistException extends Exception {
public UserNotExistException(String message) {
super(message);
}
}
模拟触发异常的请求, 只要访问/exception/username?username=aaa
就会触发.
@RestController
@RequestMapping(value = {"/exception"})
public class ExceptionTestController {
/**
* http://localhost:8082/boot1/exception/username?username=aaa
*/
@RequestMapping("/username")
@ResponseBody
public String user(@RequestParam("username") String username) throws UserNotExistException {
if ("aaa".equals(username)) {
throw new UserNotExistException("用户不存在!");
}
return username;
}
}
使用全局异常处理类来捕获异常及异常处理.
package com.aiguigu.springboot02config.exception.handler;
import com.aiguigu.springboot02config.exception.UserNotExistException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* 异常处理器
* 通知 @ControllerAdvice会扫描当前类所在包的所有处理器handler方法,执行通知增强的逻辑
* 属性 basePackages 可以指定扫描范围
*/
@ControllerAdvice(basePackages = {"com.aiguigu.springboot02config.controller"})
public class MyExceptionHandler {
// 自定义异常处理
// 浏览器客户端都返回的json数据,无法实现自适应效果(浏览器返回页面,其他客户端返回json数据)
@ResponseBody
@ExceptionHandler(UserNotExistException.class)
public Map<String, Object> handleException(Exception e) {
Map<String, Object> map = new HashMap<>();
map.put("code", "user.not_exist");
map.put("message", e.getMessage());
return map;
}
}
请求 http://localhost:8082/boot1/user?user=aaa ,浏览器和postman请求都返回自定义的json数据, 没有实现自适应响应效果。
{"code":"user.not_exist","message":"用户不存在!"}
9.4.2 自适应响应
可以将异常请求转发到/error进行自适应响应 , 在全局异常处理类中加入自定义异常信息并转发到/error
. SpringBoot默认的异常处理请求就是 /error
. (在BasicErrorController源码中查看.)
@ControllerAdvice(basePackages = {"com.aiguigu.springboot02config.controller"})
public class MyExceptionHandler {
// 对UserNotExistException进行拦截处理
@ExceptionHandler(UserNotExistException.class)
public String handleException2(Exception e, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
map.put("code", "user.not_exist");
map.put("message", "用户出错了. " + e.getMessage());
/**
* DefaultErrorAttributes#addStatus
* Integer status = (Integer)this.getAttribute(requestAttributes, "javax.servlet.error.status_code");
*/
// 传入我们自己的错误状态码, 进入我们指定的错误页面
request.setAttribute("javax.servlet.error.status_code", 500);
// 转发到 /error 实现自适应效果(浏览器返回页面,其他客户端返回json数据) ErrorMvcAutoConfiguration
return "forward:/error";
}
}
测试浏览器效果:
其他客户端请求效果:
{
"timestamp": 1682520309898,
"status": 500,
"error": "Internal Server Error",
"exception": "com.aiguigu.springboot02config.exception.UserNotExistException",
"message": "用户不存在!",
"path": "/boot1/exception/username"
}
9.4.3 传递定制数据
请求出现异常后,会来到/error
请求,会被BasicErrorController
处理,响应数据是由getErrorAttributes
得到的(父类AbstractErrorController的方法);
-
编写一个ErrorController的实现类或AbstractErrorController的子类,放在容器中。
- 异常响应数据是通过errorAttributes.getErrorAttributes方法得到的;容器中的DefaultErrorAttributes#getErrorAttributes()方法,默认进行数据处理。
- 在全局异常处理类中将需要响应的定制异常信息存入到requst域中.
在全局异常处理类中将需要传递的信息保存到自定义字段ext中.
// 对UserNotExistException进行拦截处理
@ExceptionHandler(UserNotExistException.class)
public String handleException2(Exception e, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
map.put("code", "user.not_exist");
map.put("message", "用户出错了. " + e.getMessage());
/**
* DefaultErrorAttributes
* Integer status = (Integer)this.getAttribute(requestAttributes, "javax.servlet.error.status_code");
*/
// 传入我们自己的错误状态码, 进入我们指定的错误页面
request.setAttribute("javax.servlet.error.status_code", 500);
// 将map信息存入自定义的ext字段
request.setAttribute("ext", map);
// 转发到 /error 实现自适应效果(浏览器返回页面,其他客户端返回json数据) ErrorMvcAutoConfiguration
return "forward:/error";
}
自定义MyErrorAttributes,继承DefaultErrorAttributes,重写getErrorAttributes方法,响应数据map可以存入我们自定义的信息, 还能从request域对象中取出之前存入的信息ext。
// 给容器加入自定义的 ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
// 返回值map就是页面和json能获取的所有字段
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
map.put("company", "atguigu");
// 我们的异常处理器携带的数据
Map<String, Object> ext = (Map<String, Object>) requestAttributes.getAttribute("ext", RequestAttributes.SCOPE_REQUEST);
map.put("ext", ext);
return map;
}
}
浏览器测试效果
其他客户端测试效果, 返回了company, ext自定义字段信息.
{
"timestamp": 1682519269726,
"status": 500,
"error": "Internal Server Error",
"exception": "com.aiguigu.springboot02config.exception.UserNotExistException",
"message": "用户不存在!",
"path": "/boot1/exception/username",
"company": "atguigu",
"ext": {
"code": "user.not_exist",
"message": "用户出错了. 用户不存在!"
}
}