SpringBoot 学习笔记_整合 Web 开发(一)

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 处理器。

  1. 创建 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;
    }
    
  2. 创建 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 的组合注解

  3. 运行项目,测试返回结果

    https://localhost:8080/user

除了默认的 jackson-databind JSON 转换器外,还有很多常见的转换器,如 Gsonfastjson 等。

fastjson 是阿里巴巴的开源 JSON 解析框架,是目前 JSON 解析速度最快的开源框架。该框架也可以集成到 SpringBoot 中,不同的是, fastjson 集成完成后不能立马使用,需要开发者提供相应的 HttpMessageConverter 之后才可以使用。

具体步骤如下:

  1. 去除默认的 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>
    
  2. 配置 fastjsonHttpMessageConverter

    @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;
        }
    }
    
  3. 配置响应编码,防止乱码情况发生(在 application.properties 中)。

    spring.http.encoding.force-response=true
    

    如上配置会触发警告:不建议使用该 key,建议如下:

    server.servlet.encoding.force-response=true
    
  4. 运行项目,测试返回结果

    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 中文件上传一共涉及两个组件。CommonsMultipartResolverStandardServletMultipartResolver,其中 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 中上传文件甚至可以零配置。

文件上传具体步骤如下:

  1. 添加依赖

    spring-boot-starter-web
    
  2. 创建文件上传页面(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>
    
  3. 创建文件上传处理接口

    @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 "上传失败";
        }
    }
    
  4. 启动项目,运行测试

    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 注解指定处理哪种异常。方法的参数可以有异常实例、HttpServletResponseHttpServletRequestModel 等,返回值可以是 JSONModelAndView、逻辑视图名称等。

@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 视图也没找到,就会返回默认的一个视图。

基本配置

  1. 在 resource/static 目录下创建 error 目录
  2. 新建 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.html5xx.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 方法获取的。该方法调用了 接口 ErrorAttributesgetErrorAttributes 方法,而 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 即可,而 DefaultErrorAttributesErrorAttributes 的子类,因此只需要继承 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,那么默认使用的 ErrorViewResolverDefaultErrorViewResolver,正是在 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 信息,所以跑到这里来了");
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章