SpringBoot_Chapter3 | Eloise's Paradise
0%

SpringBoot_Chapter3

本章主要介绍Springboot整合web层, 包括:

JSON处理,静态资源访问,文件上传,@ControllerAdvice用法,全局异常处理封装,自定义异常视图,CORS,加载XML,系统启动任务,web基础组件整合(拦截器, 过滤器,servlet),整合AOP. . .

Springboot 整合Jackson

JSON是目前主流的前后端数据传输方式, SpringMVC中使用消息转化器HttpMessageConverter对JSON的转换提供了很好的支持,在SpringBoot中对相关配置做了进一步简化. HttpMessageConverter既可以将服务端返回的对象序列化为JSON串, 也可以将JSON串反序列化为对象.


常见的JSON处理器除了Jackson-databind,还有Gson和fastjson. 前者是spring/springboot处理JSON的默认的自带方案. 后两者分别由Google和阿里提供, 在使用后两者前应排除掉自身的Jackson依赖以使其生效.

项目目录结构

Jackson

构建过程

新建项目

新建项目导入spring-boot-starter-web即可, 因为该starter内已经有Jackson相关依赖.

web依赖目录

准备entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.boy.springbootalljson.entity;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.util.Date;

public class User {

private Integer id;
private String username;
//@JsonFormat(pattern="yyyy/MM/dd")
private Date birth;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", birth=" + birth +
'}';
}
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username;}
public Date getBirth() { return birth; }
public void setBirth(Date birth) { this.birth = birth; }
}

准备controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.boy.springbootalljson.controller;


import com.boy.springbootalljson.entity.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

@Controller
public class UserController {
@GetMapping("/user")
@ResponseBody
public List<User> Users(){
ArrayList<User> users = new ArrayList<User>();
for (int i = 0; i <4 ; i++) {
User user = new User();
user.setId(i);
user.setUsername("Elo");
user.setBirth(new Date());
users.add(user);
}
return users;
}
}

测试

JacksonTestResult

分析

可以看出测试结果中日期是没有被格式化的. 因为Jackson的默认配置中并没有对日期进行特殊处理. 从源码可以看出这一点:

1
org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration类中有如下代码片段:
1
2
3
4
5
6
7
8
9
10
11
12
13
static class MappingJackson2HttpMessageConverterConfiguration {
MappingJackson2HttpMessageConverterConfiguration() {
}

@Bean
@ConditionalOnMissingBean(
value = {MappingJackson2HttpMessageConverter.class},
ignoredType = {"org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter", "org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter"}
)
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
return new MappingJackson2HttpMessageConverter(objectMapper);
}
}

两种解决方案

有两种方法可以解决这个问题

一种”简单”的方法就是在entity的日期属性上加一个注解,如下:

1
2
3
    //@JsonFormat(pattern="yyyy/MM/dd")
private Date birth;
...

但是如果有很多实体类中都有日期字段,都需要添加该注解就显得臃肿, 不和适宜.

一种更为简单的方式就是单独的配置日期格式.自己提供一个MappingJackson2HttpMessageConverter类型的bean, 让autoConfiguration默认配置失效, 然后再在自建的bean中进行日期格式化处理.

新建一个config类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.boy.springbootalljson.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

import java.text.SimpleDateFormat;

@Configuration
public class MyJsonConfig {


@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(){

MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
mappingJackson2HttpMessageConverter.setObjectMapper(objectMapper);
return mappingJackson2HttpMessageConverter;
}



}

使用配置类处理日期格式后的测试结果:

HttpMessageConverterTestResult

从源码片段最后的返回语句

1
return new MappingJackson2HttpMessageConverter(objectMapper);

可以看出默认配置中返回的 MappingJackson2HttpMessageConverter bean是有一个new objectMapper的参数的, 该参数是自动注入的.

往上游追可以知道该参数是JacksonAutoConfiguration类提供的, 源码片段 :

1
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperConfiguration {

@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}

}

也就是说, 可以得出如下结论:

项目中没有自己提供ObjectMapper bean时, MappingJackson2HttpMessageConverter使用的是JacksonAutoConfiguration中提供的objectmapper bean来进行构造bean并返回到容器中的.

基于这样的结论, 以及最终实现日期格式化操作是objectmapper提供的setDateFormat(format) 的这一事实, 所以, 在自动一的MyJsonConfig类中也可以之定义一个objectmapper bean并返回即可. (即: 可以用如下bean替换掉MyJsonConfig类中的 mappingJackson2HttpMessageConverter bean )

1
2
3
4
5
6
7
8

@Bean
public ObjectMapper objectMapper(){
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd hh:mm"));

return objectMapper;
}

测试结果:

objectmapperTestResult

Springboot 整合Gson

引入pom依赖

因为Gson是第三方(Google)提供的json处理方案, 所以pom要先将自身的Jackson依赖进行排除, 然后再添加Gson依赖. 具体操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>

测试依赖引入是否成功

reimport后可以看到, 此时spring-boot-starter-web中已经没有Jackson而是换成了gson

gson

Gson源码解析

从GsonHttpMessageConvertersConfiguration的源码可以看出:

1: 如果pom依赖中提供了gson, 那么GsonHttpMessageConvertersConfiguration配置就会生效, 因为该类有一个注解: @ConditionalOnClass(Gson.class)

2: 如果用户没有提供GsonHttpMessageConverter bean, 那么自动配置的GsonHttpMessageConverter bean就会生效, 其处理逻辑类似Jackson源码,只不过该bean的自动注入参数是gson而非objectmapper.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* Copyright 2012-2019 the original author or authors.
*
* https://www.apache.org/licenses/LICENSE-2.0
*/

package org.springframework.boot.autoconfigure.http;

import com.google.gson.Gson;
...

/**
* Configuration for HTTP Message converters that use Gson.
*
* @author Andy Wilkinson
* @author Eddú Meléndez
*/
@Configuration
@ConditionalOnClass(Gson.class)
class GsonHttpMessageConvertersConfiguration {

@Configuration
@ConditionalOnBean(Gson.class)
@Conditional(PreferGsonOrJacksonAndJsonbUnavailableCondition.class)
protected static class GsonHttpMessageConverterConfiguration {

@Bean
@ConditionalOnMissingBean
public GsonHttpMessageConverter gsonHttpMessageConverter(Gson gson) {
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
converter.setGson(gson); //
return converter;
}
}
.
.
.
}

自定义GsonHttpMessageConverter bean

在MyJsonConfig类中提供一个GsonHttpMessageConverter bean:

1
2
3
4
5
6
@Bean
public GsonHttpMessageConverter gsonHttpMessageConverter(Gson gson){
GsonHttpMessageConverter gsonHttpMessageConverter = new GsonHttpMessageConverter();
gsonHttpMessageConverter.setGson(new GsonBuilder().setDateFormat("yyyy/MM/dd hh:mm:ss").create());
return gsonHttpMessageConverter;
}

启动项目查看结果(如下图),从日期格式可以看出GsonHttpMessageConverter配置在生效了.

GsonTestResult

自定义Gson bean

对比Jackson的源码分析思路, 以及日期设置是在Gson参数中设置的, 可以对Gson进行同样的分析. org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration部分源码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@ConditionalOnClass(Gson.class)
@EnableConfigurationProperties(GsonProperties.class)
public class GsonAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public GsonBuilder gsonBuilder(List<GsonBuilderCustomizer> customizers) {
GsonBuilder builder = new GsonBuilder();
customizers.forEach((c) -> c.customize(builder));
return builder;
}

@Bean
@ConditionalOnMissingBean
public Gson gson(GsonBuilder gsonBuilder) {
return gsonBuilder.create();
}
...

可以看出:

1: 只要pom依赖中引入了Gson, GsonAutoConfiguration配置就会会生效, 因为其被注解 @ConditionalOnClass(Gson.class)修饰

2: GsonHttpMessageConverter bean中的gson参数是从此处的Gson构造引入的,

所以类比整合Jackson时可以只在配置文件中提供一个objectmapper bean的情况, 可以推断, 整合gson也可以在MyJsonConfig类中只提供一个Gson bean, 应该也能实现同样的整合配合.

在MyJsonConfig注释掉GsonHttpMessageConverter bean, 然后 提供如下bean后重启项目测试:

1
2
3
4
@Bean
public Gson gson(GsonBuilder gsonBuilder) {
return gsonBuilder.setDateFormat("yyyy-MM-dd").create();
}

测试结果:

GsonTestResult

整合FastJson

引入依赖

因为Fastjson是第三方(阿里)提供的json处理方案, 所以pom要先将自身的Jackson依赖进行排除, 但你果然也要将志气啊添加的Gson排除, 然后再添加fastjson依赖. 具体操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>

测试依赖引入是否成功

reimport后可以看出只存在fastjson依赖相关的jar包了

fastjsonpom

源码分析

与Jackson和Gson不同的是, 要整合fastjson, 则必须引入FastJsonHttpMessageConverter bean, 因为没有starter中并没有相关的autoconfiguration.

源码:

1
2
3
4
5
6
7
8
9
public class FastJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object>{
public final static Charset UTF8 = Charset.forName("UTF-8");
private Charset charset = UTF8;
private SerializerFeature[] features = new SerializerFeature[0];
public FastJsonHttpMessageConverter(){
super(new MediaType("application", "json", UTF8), new MediaType("application", "*+json", UTF8));
}
...
}

可以看见,

1: FastJsonHttpMessageConverter 类和其构造方法并没有被任何@ConditionalOnMissingBean(…)或@ConditionalOnBean(…)之类的注解修饰,

2: 构造方法为空参构造, 也就是说只能提供FastJsonHttpMessageConverter bean进行自定义配置, 而不能像Gson只提供gson bean或者Jackson只提供objectmapper bean那样进行配置.

自定义FastJsonHttpMessageConverter

在MyJsonConfig类中提供一个FastJsonHttpMessageConverter bean后重启项目; (注意fastjson的配置是在FastJsonConfig中设置的, 这一点与Gson和jackson稍有不同)

1
2
3
4
5
6
7
8
9
@Bean
public FastJsonHttpMessageConverter fastJsonHttpMessageConverter(){
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setDateFormat("yyyy-MM-dd");
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
return fastJsonHttpMessageConverter;

}

测试结果

测试结果如下图:

FastJsonResult

静态资源访问

在 Spring Boot 中,如果我们是从 https://start.spring.io 这个网站上创建的项目,或者使用 IntelliJ IDEA 中的 Spring Boot 初始化工具创建的项目,默认都会存在 resources/static 目录,很多小伙伴也知道静态资源只要放到这个目录下,就可以直接访问,除了这里还有没有其他可以放静态资源的位置呢?为什么放在这里就能直接访问了呢?

整体规划

在 Spring Boot 中,默认情况下,一共有5个位置可以放静态资源,五个路径分别是如下5个:

  1. classpath:/META-INF/resources/
  2. classpath:/resources/
  3. classpath:/static/
  4. classpath:/public/
  5. /

前四个目录好理解,分别对应了resources目录下不同的目录,第5个 / 是啥意思呢?我们知道,在 Spring Boot 项目中,默认是没有 webapp 这个目录的,当然我们也可以自己添加(例如在需要使用JSP的时候),这里第5个 / 其实就是表示 webapp 目录中的静态资源也不被拦截。如果同一个文件分别出现在五个目录下,那么优先级也是按照上面列出的顺序。

不过,虽然有5个存储目录,除了第5个用的比较少之外,其他四个,系统默认创建了 classpath:/static/ , 正常情况下,我们只需要将我们的静态资源放到这个目录下即可,也不需要额外去创建其他静态资源目录,例如我在 classpath:/static/ 目录下放了一张名为A.png 的图片,那么我的访问路径是:

1
http://localhost:8080/1.png
1
**注意,请求地址中并不需要 static,加上static反而多此一举会报404错误。那为什么不需要添加 static呢?资源明明放在 static 目录下。**

源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
//...
private static final String[] SERVLET_LOCATIONS = { "/" };
//...
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;
}
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;
}
//...
/**
* 该方法实现自WebMvcConfigurer接口, 其中方法注释描述如下
* Add handlers to serve static resources such as images, js, and, css
* files from specific locations under web application root, the classpath,
* and others.
*/
@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));
}
}
//...
}

从上述源码逻辑可以看出:

这里静态资源的定义和我们前面提到的Java配置SSM中的配置非常相似,其中,,this.resourceProperties.getStaticLocations()方法返回了四个位置,分别是:

1
2
3
4
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/

然后在getResourceLocations方法中,又添加了“/”,因此这里返回值一共有5个。

其中,/表示webapp目录,即webapp中的静态文件也可以直接访问。静态资源的匹配路径按照定义路径优先级依次降低。因此这里的配置和我们前面提到的如出一辙。这样大伙就知道了为什么Spring Boot 中支持5个静态资源位置,同时也明白了为什么静态资源请求路径中不需要/static,因为在路径映射中已经自动的添加上了/static了。

测试求证

新建一个项目, 在如下目录下新建一个hi.html文件, 请求:

1
http://localhost:8081/lee/hi.html

前端返回:

最高优先级

也就是说: classpath:/META-INF/resources/ 具有最高优先级, 然后将该目录下的hi.html重命名或删掉再次请求

1
http://localhost:8081/lee/hi.html

这次前端返回:

最高优先级

重复上述步骤 可以得知优先级如下:

1
2
3
4
classpath:/META-INF/resources/
classpath:/static/
classpath:/public/
classpath:/resources/

自定义静态资源访问路径

当然,这个是系统默认配置,如果我们并不想将资源放在系统默认的这五个位置上,也可以自定义静态资源位置和映射,自定义的方式也有两种,可以通过 application.properties 来定义,也可以在 Java 代码中来定义,下面分别来看。

方法一:配置文件properties

在配置文件application.properties中定义的方式比较简单,如下:

1
2
spring.resources.static-locations=classpath:/
spring.mvc.static-path-pattern=/**

方法二:配置类config

1
2
3
4
5
6
7
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/aaa/");
}
}
1
2
** 注意:**
整合了 Thymeleaf 的springboot项目,会将静态资源也放在 resources/templates 目录下,**但是 templates 目录并不是静态资源目录**,它是一个放页面模板的位置(你看到的 Thymeleaf 模板虽然后缀为 .html,其实并不是静态资源).

异常处理

默认现象

a)浏览器

b) 其他客户端(postman)

原理分析:

相关配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

//1. DefaultErrorAttributes:
帮我们在页面共享信息;
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, requestAttributes);
addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
addPath(errorAttributes, requestAttributes);
return errorAttributes;
}
//2.BasicErrorController:处理默认/error请求
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

@RequestMapping(produces = "text/html")//产生html类型的数据;浏览器发送的请求来到这个方法处理
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());

//去哪个页面作为错误页面;包含页面地址和页面内容
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}

@RequestMapping
@ResponseBody //产生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);
}
//3、ErrorPageCustomizer:
@Value("${error.path:/error}")
private String path = "/error"; 系统出现错误以后来到error请求进行处理;(web.xml注册的错误页面规则)
//4、DefaultErrorViewResolver:
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}

private ModelAndView resolve(String viewName, Map<String, Object> model) {
//默认SpringBoot可以去找到一个页面? error/404
String errorViewName = "error/" + viewName;

//模板引擎可以解析这个页面地址就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
//模板引擎可用的情况下返回到errorViewName指定的视图地址
return new ModelAndView(errorViewName, model);
}
//模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/404.html
return resolveResource(errorViewName, model);
}
//5. DefaultErrorViewResolver
protected ModelAndView resolveErrorView(HttpServletRequest request,
HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
//所有的ErrorViewResolver得到ModelAndView
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}

定制Error信息

定制错误页面Page:

如果项目有整合模板引擎thymeleaf, 那么直接在templates目录下添加error目录, 并在里面定制404.html, 405.html,4xx.html这样的模板页面即可, 注意错误发生时, 会按照view的code先精确匹配, 无法精确匹配的情况下才模糊匹配(如找不到503错误, 则会找5XX):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
<meta charset="UTF-8">
<title>5XX</title>
</head>

<body>
<h1>5**, yet not 503!</h1>
<p>状态码status: [[${status}]]</p>
<p>时间timestamp: [[${timestamp}]]</p>
<p>错误信息error: [[${error}]]</p>
<p>exception: [[${exception}]]</p>
<p>异常信息: [[${message}]]</p>
<p>异常信息errors: [[${errors}]]</p>
<p>parameter: [[${parameter}]]</p>
</body>

</html>

有模板引擎

没有模板引擎, 但静态资源目录下有error

什么都没有

定制错误数据Json:

a. 封装Exception:

1
2
3
4
5
6
7
8
package com.lee.exception.exception;

public class UserNotFoundException extends RuntimeException {

public UserNotFoundException() {
super("用户不存在 !");
}
}

定义全局异常处理类, 在全局异常处理类里面进行数据封装返回:

方式一:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.lee.exception.exp;

import com.lee.exception.exception.UserNotFoundException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class MyGlobalExceptionHandler {

//方式一: JSON数据直接返回, 但是没有自适应效果
@ResponseBody //无论浏览器还是其他客户端(postman等)都是返回的JSON数据 而且没有
@ExceptionHandler(UserNotFoundException.class)
public Map<String,Object> userNotFoundHandler(Exception e){
Map<String, Object> map = new HashMap<>();
map.put("code","UNF");
map.put("msg","查无此用户");
map.put("details",e.getMessage());
return map;
}

}

现象:

方式一

说明:

1
方式一是JSON数据直接返回, 但是没有自适应效果, 因为指定了@ResponseBody, 所以无论浏览器还是其他客户端(postman等)都是返回的JSON数据

方式二:

代码:

1
2
3
4
5
6
7
8
@ExceptionHandler(UserNotFoundException.class)
public String userNotFoundHandler(Exception e){
Map<String, Object> map = new HashMap<>();
map.put("code","UNF");
map.put("msg","查无此用户");
map.put("details",e.getMessage());
return "forward:/error";
}

现象:

方式二

说明:

1
虽然有自适应效果, 但是状态码确是200, 所以也不会进入我们自定义的页面, 而是进入springboot准备的默认页面.

方式三:

代码:

1
2
3
4
5
6
7
8
9
10
@ExceptionHandler(UserNotFoundException.class)
public String userNotFoundHandler(Exception e, HttpServletRequest request){
Map<String, Object> map = new HashMap<>();
request.setAttribute("javax.servlet.error.status_code",500); /** 这不是必须的*/
map.put("code","UNF");
map.put("msg","查无此用户");
map.put("details",e.getMessage());
request.setAttribute("myErrorInfo",map);
return "forward:/error";
}

​ 这种方式在写好全局异常处理方法userNotFoundHandler后, 还需要一个自定义的错误属性封装类, 继承自DefaultErrorAttributes, 具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.lee.exception.component;

import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;

import java.util.Map;

@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
errorAttributes.put("attribute1_company","Apache");
Map<String,Object> exp= (Map<String,Object>) webRequest.getAttribute("myErrorInfo",0);
errorAttributes.put("exp",exp);
return errorAttributes;
}
}

然后再前端模板error/5xx.html取出展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
<meta charset="UTF-8">
<title>5XX</title>
</head>

<body>
<h1>5**, yet not 503!</h1>
<p>状态码status: [[${status}]]</p>
<p>时间timestamp: [[${timestamp}]]</p>
<p>错误信息error: [[${error}]]</p>
<p>exp: [[${exp}]]</p> <!-- 后端封装到域的是什么属性, 这里就取出什么 -->
</body>

</html>

现象:

方式三

方式三postman请求

说明:

1
必须要通过request.setAttribute("javax.servlet.error.status_code",500);设置自定义的错误状态码, 才会进入响应的错误页面, 如果需要自定义封装错误信息, 需要首先将自定义通过request.setAttribute("myErrorInfo",map);的方式放到域中, 然后再在容器中定义一个错误属性类, 获取域中的错误属性并返回

@ControllerAdvice补充

除了上面介绍的全局异常处理外, @ControllerAdvice 注解还有两外两种用法.

a). 参数预处理

b).

SpringBoot-AOP

-------------本文结束感谢您的阅读-------------