SpringBoot 学习笔记_整合 Web 开发(一)
声明:
本次学习参考 《SpringBoot + Vue 开发实战》 · 王松(著) 一书。
本文的目的是记录我学习的过程和遇到的一些问题以及解决办法,其内容主要来源于原书。
如有侵权,请联系我删除
文章目录
SpringBoot 整合 Web 开发
返回 JSON 数据
JSON 是目前主流的前后端数据传输方式, Spring MVC 中使用消息转换器
HttpMessageConverter
对 JSON 的转换提供了很好的支持。在 SpringBoot 中,对相关配置做了进一步优化。
SpringBoot 环境搭建时,默认添加的 Web 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
该依赖中默认加入了 jackson-databind
作为 JSON 处理器。
-
创建 User 实体类
/** * jackson-databind JSON 处理器使用 */ public class User { private String username; private String password; @JsonIgnore private String address; @JsonFormat(pattern = "yyyy-MM-dd") private Date birthday; }
-
创建 UserController 控制器,返回 User 对象
@Controller public class UserController { @GetMapping("/user") @ResponseBody public User user(){ User user = new User(); user.setUsername("Ambrose"); user.setPassword("123456"); user.setAddress("广东省深圳市"); user.setBirthday(new Date()); return user; } }
依稀记得,在 Book 类中,我们使用了 @RestController 注解,但在这里却使用了 @Controller 注解,这两个有什么关系呢?
其实, @RestController 是 @Controller 和 @ResponseBody 的组合注解
-
运行项目,测试返回结果
https://localhost:8080/user
除了默认的
jackson-databind
JSON 转换器外,还有很多常见的转换器,如Gson
、fastjson
等。
fastjson
是阿里巴巴的开源 JSON 解析框架,是目前 JSON 解析速度最快的开源框架。该框架也可以集成到 SpringBoot 中,不同的是,fastjson
集成完成后不能立马使用,需要开发者提供相应的HttpMessageConverter
之后才可以使用。
具体步骤如下:
-
去除默认的
jackson-databind
依赖,引入fastjson
依赖。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!-- 去除该依赖中默认的 jackson-databind 依赖,再在后面引入 fastjson 依赖 --> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </exclusion> </exclusions> </dependency> <!-- 引入 fastjson 依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency>
-
配置
fastjson
的HttpMessageConverter
。@Configuration public class MyFastJsonConfig { @Bean FastJsonHttpMessageConverter fastJsonHttpMessageConverter(){ FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); FastJsonConfig config = new FastJsonConfig(); config.setDateFormat("yyyy-MM-dd"); config.setCharset(Charset.forName("UTF-8")); config.setSerializerFeatures( SerializerFeature.WriteClassName, SerializerFeature.WriteMapNullValue, SerializerFeature.PrettyFormat, SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteNullStringAsEmpty ); converter.setFastJsonConfig(config); return converter; } }
-
配置响应编码,防止乱码情况发生(在
application.properties
中)。spring.http.encoding.force-response=true
如上配置会触发警告:不建议使用该 key,建议如下:
server.servlet.encoding.force-response=true
-
运行项目,测试返回结果
https://localhost:8080/user
或者也可以这样进入:
在 SpringBoot 项目中,引入
spring-boot-starter-web
依赖,该依赖又依赖了spring-boot-autoconfig
,而在这个自动化配置中,有一个WebMvcAutoConfiguration
的类提供了对SpringMVC
的基本配置,如果某一自动化配置无法满足开发需求,可以通过重新实现WebMvcConfigurer
接口来自定义:
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
FastJsonConfig config = new FastJsonConfig();
config.setDateFormat("yyyy-MM-dd");
config.setCharset(Charset.forName("UTF-8"));
config.setSerializerFeatures(
SerializerFeature.WriteClassName,
SerializerFeature.WriteMapNullValue,
SerializerFeature.PrettyFormat,
SerializerFeature.WriteNullListAsEmpty,
SerializerFeature.WriteNullStringAsEmpty
);
converter.setFastJsonConfig(config);
converters.add(converter);
}
}
静态资源访问
SpringBoot
中对于Spring MVC
的自动化配置都在WebMvcAutoConfiguration
类中。
在WebMvcAutoConfiguration
类中有一个静态内部类WebMvcAutoConfigurationAdapter
,实现了WebMvcConfigurer
接口,WebMvcConfigurer
接口中有一个方法addResourceHandlers
就是用来配置静态资源过滤的。
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
springBoot 就是在这里进行了默认的静态资源默认配置,其中 staticPathPattern
默认定义 WebMvcProperties
中,定义如下:
private String staticPathPattern = "/**";
this.resourceProperties.getStaticLocations()
获取到的默认静态资源位置定义在 ResourceProperties
中:
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
};
在 getResourceLocations
方法中,对这 4 个静态资源位置做了扩充:
static String[] getResourceLocations(String[] staticLocations) {
String[] locations = new String[staticLocations.length + SERVLET_LOCATIONS.length];
System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length);
System.arraycopy(SERVLET_LOCATIONS, 0, locations, staticLocations.length, SERVLET_LOCATIONS.length);
return locations;
}
其中,SERVLET_LOCATIONS
的定义如下:
private static final String[] SERVLET_LOCATIONS = { "/" };
也就是说,开发者可以将静态资源放到这 5 个位置中的任意一个(注意:按照定义的顺序, 5 个位置优先级依次降低,但是一般情况下,SpringBoot 项目不需要 webapp
目录,所以第 5 个 “/” 可以不用考虑)
如果默认的静态资源过滤策略不能满足要求,也可以自定义静态资源过滤策略
自定义静态资源过滤策略有两种方式:
-
配置文件中定义:
在
application.properties
中直接定义过滤规则和静态资源位置# 过滤规则 spring.mvc.static-path-pattern=/static/** # 静态资源位置 spring.resources.static-location=classpath:/static/
-
Java
编码定义需要实现
WebMvcConfigurer
接口,然后实现接口的addResourceHandlers
方法@Configuration public class MyWebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHanlderRegistry registry) { registry .addResourceHandler("/static/**") .addResourceLocations("classpath:/static/") } }
文件上传
Java 中文件上传一共涉及两个组件。
CommonsMultipartResolver
和StandardServletMultipartResolver
,其中CommonsMultipartResolver
使用commons-fileupload
来处理multipart
请求,而StandardServletMultipartResolver
则是基于 Servlet 3.0 来处理multipart
请求的。因此,如果使用
StandardServletMultipartResolver
,则不需要添加额外的 jar 包。Tomcat 7.0 就已经支持 Servlet 3.0 了,而 SpringBoot 2.0.4 内嵌 Tomcat 为 Tomcat 8.5.32,所以可以直接使用
StandardServletMultipartResolver
,而在 SpringBoot 提供的文件上传自动化配置类MultipartAutoConfiguration
中,默认也是采用的StandardServletMultipartResolver
。
从 MultipartAutoConfiguration
的源码可以看出:如果开发者没有提供 MultipartResolver
,那么默认采用的 MultipartResolver
就是 StandardServletMultipartResolver
。因此,在 SpringBoot 中上传文件甚至可以零配置。
文件上传具体步骤如下:
-
添加依赖
spring-boot-starter-web
-
创建文件上传页面(
upload.html
)在
resources
目录下的static
目录中创建一个upload.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>文件上传测试</title> </head> <body> <form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="uploadFile" value="请选择文件"> <input type="submit" value="上传"> </form> </body> </html>
-
创建文件上传处理接口
@RestController public class FileUploadController { SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); @PostMapping("/upload") public String upload(MultipartFile uploadFile, HttpServletRequest request) { /* 保存上传文件的目录 */ String realPath = request.getSession().getServletContext().getRealPath("/uploadFile/"); String format = sdf.format(new Date()); /* 在保存文件目录下准备创建日期相关文件夹,以日期归档 */ File folder = new File(realPath + format); if (!folder.isDirectory()) folder.mkdirs(); /* 原始文件名 */ String oldName = uploadFile.getOriginalFilename(); /* 新文件名 */ String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."), oldName.length()); try { /* 文件保存 */ uploadFile.transferTo(new File(folder, newName)); /* 返回文件访问路径 */ String filePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() +"/uploadFile/" + format + newName; return filePath; }catch (IOException e){ e.printStackTrace(); } return "上传失败"; } }
-
启动项目,运行测试
https://localhost:8080/upload
问题记录:
这里,遇到一个问题: 上传成功并返回路径,但项目中并未看到文件
首先,通过断点排查,发现
String realPath = request.getSession().getServletContext().getRealPath("/uploadFile/");
返回的结果并不是期望的项目根目录,而是一个位于 tomcat.xxxx.port(服务端口号) 的临时目录下,如下:
C:\Users\Administrator\AppData\Local\Temp\tomcat-docbase.6275041815373391706.8080\uploadFile\2020\06\22
查阅资料后发现这样一行代码
private static final String[] COMMON_DOC_ROOTS = { "src/main/webapp", "public","static" };
SpringBoot 在获取路径时会扫描该配置中的文件夹,所以尝试在项目根目录中新建一个 public 目录,再次测试,发现成功,
realPath
也返回了正确的地址。
根目录 ‘/’ 是静态资源地址,可以直接访问,但是像上面加了 pulbic 或者 static 目录之后,如果还想要通过 URL 静态访问这个地址,就需要将该目录页加入静态资源目录列表
全局处理
@ControllerAdvice
注解,是@Controller
的增强版,其主要作用,就是用来做全局处理,一般搭配@ExceptionHandler
、@ModelAttribute
和@InitBinder
使用。
全局异常
只需要定义一个类,添加 @ControllerAdvice
注解。当系统启动时,该类就会被扫描到 Spring 容器,然后定义 uploadException
方法,该方法上可以添加 @ExceptionHandler
注解指定处理哪种异常。方法的参数可以有异常实例、HttpServletResponse
与 HttpServletRequest
、Model
等,返回值可以是 JSON
、ModelAndView
、逻辑视图名称等。
@ControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ModelAndView uploadException(MaxUploadSizeExceededException e, HttpServletResponse resp) throws IOException{
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("msg", "上传文件大小超出");
modelAndView.setViewName("error");
return modelAndView;
}
}
全局数据
@ControllerAdvice
作为一个全局数据处理组件,处理全局异常只是最常用的场景之一,当然也可以用来配合@ModelAttribute
处理全局数据。
@ControllerAdvice
public class GlobalConfig {
// value 属性表示该条数据的 key, 返回值为该条数据的 value
// 此时,在任意 Controller 下都可以通过参数中的 Model 获取到 info 的数据。
@ModelAttribute(value = "info")
public Map<String, String> userinfo(){
HashMap<String, String> map = new HashMap<>();
map.put("username", "AmbroseCdMeng");
map.put("gender","男");
return map;
}
}
@RestController
public class HelloController {
public void hello(Model model) {
Map<String, Object> map = model.asMap();
Set<String> keySet = map.keySet();
Iterator<String> iterator = keySet.iterator();
while (iterator.hasNext()) {
String key = iterator.next();
Object value = map.get(key);
System.out.println(key + " >>>>>>>>>> " + value);
}
}
}
请求参数预处理
@ControllerAdvice
还有另一个比较常用的场景,就是结合@InitBinder
实现请求参数预处理,即将表单中的数据绑定到实体类上时进行一些额外处理。
比如,两个实体类都有 name
属性,控制器中需要接受两个实体类,在参数传递时,两个 name
就会混淆。使用 @ControllerAdvice
结合 @InitBinder
就可以顺利解决该问题。
@GetMapping("/book")
@ResponseBody
public String book(@ModelAttribute("b") Book book, @ModelAttribute("a")Author author){
return book.toString() + " >>>>>>>>>> " + author.toString();
}
访问测试:
https://localhost:8080/book?b.name=bookname&a.name=authorname
自定义错误页
全局异常虽然可以根据实际情况返回不同的页面,但是这种异常处理方式一般用来处理应用级别的异常,有一些容器级别的错误处理不了。
而这些情况,默认是由 SpringBoot 的
BasicErrorController
类来处理的。
BasicErrorController
类的核心方法有两个:errorHtml
(用来返回错误 HTML 页面) 和error
(用来返回错误 JSON)
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
SpringBoot 默认会在 error 目录下查找 4xx、5xx 的文件作为错误视图,如果找不到会回到 errorHtml 方法中,然后使用 error 作为默认的错误视图,如果 error 视图也没找到,就会返回默认的一个视图。
基本配置
- 在 resource/static 目录下创建 error 目录
- 新建 4xx.html、5xx.html 页面,或者更详细一些直接以响应码命名,如:404.html、500.html
由于这种定义都是静态 HTML 页面,无法向用户展示详细的错误信息。
所以,还可以采用 HTML 模板,以 Thymeleaf 为例, Thymeleaf 的模板位于 classpath:/templates
目录下,因此,在该目录下创建错误展示页。
制造一个异常来测试
@GetMapping("/errorHello")
public int errorHello(){
/* 抛出异常:测试错误页面效果 */
return 1 / 0 ;
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table border="1">
<tr>
<td>timestamp</td>
<td th:text="${timestamp}"></td>
</tr>
<tr>
<td>status</td>
<td th:text="${status}"></td>
</tr>
<tr>
<td>error</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>message</td>
<td th:text="${message}"></td>
</tr>
<tr>
<td>path</td>
<td th:text="${path}"></td>
</tr>
</table>
</body>
</html>
- 第一次测试:返回了全局异常中对应的
error
页面,这说明在全局异常能处理的情况下,其优先级是最高的; - 将全局异常屏蔽,第二次测试:返回
resouce/static/error
目录下的 500.html 页面; - 在
resource/static/error
目录下新增一个 5xx.html 页面,第三次测试:返回resouce/static/error
500.html 页面,这说明,同目录下,响应码.html
优先于4xx.html
或者5xx.html
- 在
template/error
目录中新建4xx.html
和5xx.html
页面,第四次测试:返回resouce/static/error
500.html 页面,这说明,不同目录下,响应码.html
依然优先于4xx.html
或者5xx.html
- 删除
resouce/static/error
目录下的500.html
页面,第五次测试:返回template/error
下的5xx.html
页面,这说明,文件名相同时,动态页面优先级高于静态页面
综上,优先级从高到低依次为:全局异常(能处理的范围内) > 动态响应码.html > 静态响应码.html > 动态4xx/5xx.html > 静态4xx/5xx.html
复杂配置
所谓动态模板页面,其实也只返回了 5 条固定信息而已。如果想自定义 Error 数据,该如何操作?
我们知道,errorHtml 和 error 放在在 BasicErrorController
方法中,进入该方法,可以看到,Error 信息都是通过 getErrorAttributes
方法获取的。该方法调用了 接口 ErrorAttributes
的 getErrorAttributes
方法,而 ErrorAttributes
接口的 getErrorAttributes
方法在 DefaultErrorAttributes
中实现的。
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
if (this.includeException != null) {
options = options.including(new Include[]{Include.EXCEPTION});
}
if (!options.isIncluded(Include.EXCEPTION)) {
errorAttributes.remove("exception");
}
if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
errorAttributes.put("message", "");
}
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
return errorAttributes;
}
继续追查
DefaultErrorAttributes
, 发现是在ErrorMvcAutoConfiguration
中使用的
...
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
...
public class ErrorMvcAutoConfiguration {
...
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
...
}
@ConditionalOnMissingBean
可以看出,当系统没有提供ErrorAttributes
才会采用DefaultErrorAttributes
。因此,自定义错误提示时,只需要自己提供一个
ErrorAttributes
即可,而DefaultErrorAttributes
是ErrorAttributes
的子类,因此只需要继承DefaultErrorAttributes
即可。
@Component
public class MyErrorAttribute extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
errorAttributes.put("message", "我在 MyErrorAttribute 中定义了自定义 Error 页面,所以到这里来了");
errorAttributes.remove("error");
return errorAttributes;
}
}
另外,同样,可以从源码中看到,如果用户没有定义
ErrorViewResolver
,那么默认使用的ErrorViewResolver
是DefaultErrorViewResolver
,正是在DefaultErrorViewResolver
中配置了默认取 error 目录下寻找 4xx.html、5xx.html。如果开发者想要自定义 Error 视图,只需要提供自己的 ErrorViewResolver 即可
@Component
public class MyErrorViewResolver implements ErrorViewResolver {
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView mv = new ModelAndView("errorPage");
mv.addObject("message", "我在 MyErrorViewResolver 中定义了自定义 Error 页面,所以跑到这里来了");
mv.addAllObjects(model);
return mv;
}
}
接下来在 resources/templates 目录下定义 errorPage.html
即可
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>这里是自定义的 Error 页面</h3>
<table border="1">
<tr>
<td>timestamp</td>
<td th:text="${timestamp}"></td>
</tr>
<tr>
<td>status</td>
<td th:text="${status}"></td>
</tr>
<tr>
<td>error</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>message</td>
<td th:text="${message}"></td>
</tr>
<tr>
<td>path</td>
<td th:text="${path}"></td>
</tr>
</table>
</body>
</html>
如果自定义 Error 数据和自定义 Error 页面仍然不足以满足开发需求,那么还可以完全自定义 Error 内容
可以发现
BasicErrorController
本身只是一个默认的配置,若开发这没有提供自己的ErrorController
,则 SpringBoot 将提供BasicErrorController
作为默认的ErrorController
。如果想要灵活的自定义,只需要提供自己的ErrorController
即可。
提供自定义的 ErrorController
有两种方式:一种是实现 ErrorController
接口,另一个是直接继承 BasicErrorController
。
接下来,自定义 ErrorController
继承 BasicErrorController
来实现:
public class MyErrorController extends BasicErrorController {
public MyErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
super(errorAttributes, errorProperties);
}
public MyErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorProperties, errorViewResolvers);
}
@Override
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML));
model.put("message", "我在 MyErrorController 中定义了自定义 ErrorHtml 信息,所以跑到这里来了");
ModelAndView modelAndView = new ModelAndView("myErrorPage", model, status);
return modelAndView;
}
@Override
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
body.put("message","我在 MyErrorController 中定义了自定义 Error 信息,所以跑到这里来了");
return new ResponseEntity<>(body, status);
}
}
最后在 resources/templates
目录下提供 myErrorPage.html
页面作为视图页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>这里是自定义的 Error 页面</h3>
<table border="1">
<tr>
<td>timestamp</td>
<td th:text="${timestamp}"></td>
</tr>
<tr>
<td>status</td>
<td th:text="${status}"></td>
</tr>
<tr>
<td>error</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>message</td>
<td th:text="${message}"></td>
</tr>
<tr>
<td>path</td>
<td th:text="${path}"></td>
</tr>
</table>
</body>
</html>
以上在 `MyErrorController` 中
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
一行提示已经过时,会建议修改如下
Map<String, Object> body = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)));
但是 Colloections.unmodifiabelMap
获取到的 Map
对象是禁止修改的,所以如下代码会抛出 UnsupportedOperationException
异常
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
model.put("message", "我在 MyErrorController 中定义了自定义 ErrorHtml 信息,所以跑到这里来了");