本篇是Web开发整合的中篇内容,具体包括:自定义错误页、CORS支持、配置类与XML配置和注册拦截器等相关知识。

自定义错误页

在上篇中介绍了使用@ControllerAdvice配和@ExceptionHandler注解来处理全局异常。在处理异常时,开发者可以根据实际情况返回不同的页面,但是这种异常处理方式一般用来处理应用级别的异常,有一些容器级别的错误就处理不了,如Filter中抛出异常,使用@ControllerAdvice定义的全局异常处理机制就无法处理,很高兴SpringBoot开发团队意思到了这一点,提供了其他的异常处理方式。

通过前面的学习在访问请求过程中,多多少少也碰到过404和500等页面,它们就是根据用户在发起请求时发生的错误类型来返回不同的页面:

使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为errorspringboot,然后添加spring-boot-starter-web依赖。

实际上,SpringBoot在返回错误信息的时候不一定返回HTML页面,就像前面设置的当你返回String对象就是JSON对象,返回ModelAndView对象就是HTML页面,这个需要根据实际情况来进行返回,再比如当你发起ajax请求,则错误信息是一段JSON。对于开发者来说,这一段HTML或者JSON都是可以自由定制。

SpringBoot中的错误默认是由BasicErrorController类来处理的,查看源码可知该类有两个核心方法,分别为:

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
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {

......

@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}

其中的errorHtml方法用来返回错误的HTML页面,error方法用来返回错误JOSN,因此具体返回的是HTML还是JSON需要看请求头的Accept参数,其实就是MediaType的类型。返回JSON的逻辑很简单,一眼就能看懂,关键是返回HTML的逻辑稍微有些复杂,因为涉及到视图解析器。

在errorHtml方法中,通过调用resolveErrorView方法来获取一个错误视图ModelAndView。而resolveErrorView方法的调用最终会来到DefaultErrorViewResolver类中。DefaultErrorViewResolver类是SpringBoot中默认的错误信息视图解析器,可以阅读其源码,这里粘贴部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
......

private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
......
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
}

从上面的源码可以看出,SpringBootmo默认是在error目录下查找4xx、5xx的文件作为错误图,当找不到时会返回errorHtml方法中,然后使用error作为默认的错误页面视图名称,如果名为error的视图也找不到,那么就会看到前面展示的两个错误提示页面,这就是整个错误处理的大概流程。

简单配置

通过前面的介绍,可以发现其实自定义错误页面非常简单,只需要提供4xx和5xx页面即可。如果开发者不需要向用户展示详细的错误信息时,可以将错误信息定义为静态页面。在resources/static目录下新建一个error目录,然后在error目录中创建自定义错误信息的html页面。错误信息展示页面的命名规则有两种:第一种是4xx.html和5xx.html;另一种是直接使用响应码来命名文件,如401.html、404.html和500.html等。很显然第二种命名方式显得更为具体,当程序运行出错时,由于不同的错误会展示不同的错误页面,因此一眼就知道程序是出了什么Bug:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// resources\static\error\404.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404</title>
</head>
<body>
<div>this is a page for 404</div>
</body>
</html>

// resources\static\error\500.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>500</title>
</head>
<body>
<div>this is a page for 500</div>
</body>
</html>

启动项目,当用户访问一个不存在的路径时,刚才定义的404页面就会被展示出来了:

4xx是客户端的错误,5xx是服务器的错误,也就是java程序的错误,因此可以创建一个controller包,并在里面新建一个TestController,里面定义一个test方法,该方法只要抛出一个异常即可,最简单的就是算数除法除数为0了:

1
2
3
4
5
6
7
8
9
@RestController
public class TestController {

@GetMapping("/test")
public String test(){
int i = 1/0;
return "test";
}
}

运行项目,访问该链接即可将刚才定义的500页面展示出来:

前面使用的都是静态HTML页面,无法向用户展示完整的错误信息,如果想展示更多的信息,可以使用视图模板技术。如果想使用HTML模板,那么需要在pom.xml中引入模板的相关依赖,这里以Thymeleaf为例进行说明。

在pom.xml中导入Thymeleaf依赖:

1
2
3
4
5
  <!--添加Thymeleaf依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

请注意Thymeleaf页面模板默认处于classpath:/templates/目录下,因此需要在该目录下先创建error目录,再创建错误展示页,如图所示:

由于模板页面信息展示较为灵活,因此只需要创建4xx.html和5xx.html文件即可。以4xx.html为例说一下其中的内容:

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
<!DOCTYPE html>
<html lang="en" xmlns="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>4xx</title>
</head>
<body>
<table border="1px">
<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>

SpringBoot在这里一个返回了5条错误相关的信息,分别是timestamp、status、error、message和path。注意5xx.html页面中的内容与4xx.html文件中的内容完全一致。

启动项目,当用户访问一个不存在的地址时,就会将4xx.html页面中设置的信息给显示出来:

当用户访问一个会抛异常的地址(该地址必须存在),如前面的test接口时,就会展示5xx.html页面内的信息:

特别注意:当用户定义了多个错误页面,则响应编码.html页面的优先级高于4xx.html、5xx.html页面的优先级。举个例子来说,若当前是一个404错误,那么优先展示的是404.html,而不是4xx.html;同时动态页面的优先级高于静态页面。即如果resources/static/resources/templates/目录下同时定义了4xx.html,则优先展示resources/templates/目录下的4xx.html文件。

复杂配置

上面介绍的这种配置还不够灵活,只能定义HTML页面,无法处理JOSN,特别是JSON的定制需求。SpringBoot支持对Error信息的深度定制,接下来将从三个方面介绍深度定制:自定义Error数据、自定义Error视图以及完全自定义。

1、自定义Error数据。所谓的自定义Error数据其实就是对返回的数据进行自定义。在上面的介绍中,你也知道SpringBoot返回的Error信息一共有5条,分别是timestamp、status、error、message以及path。在BasicErrorController中,无论是errorHtml还是error方法,都是通过getErrorAttributes方法来获取Error信息的。点进去发现这个getErrorAttributes方法来自AbstractErrorController这个类,该类中的getErrorAttributes方法其实调用的是errorAttributes.getErrorAttributes()方法:

1
2
3
4
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
WebRequest webRequest = new ServletWebRequest(request);
return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
}

继续点进去查看,找到了private final ErrorAttributes errorAttributes;再点进去查看,发现这个ErrorAttributes是一个接口:

1
2
3
4
5
public interface ErrorAttributes {
Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace);

Throwable getError(WebRequest webRequest);
}

这个接口中虽然有getErrorAttributes方法,但是它只需传入webRequestincludeStackTrace对象,但是includeStackTrace对象具体是什么,这个也没有指明,说明此处的getErrorAttributes方法在其他的类中存在不同方式的实现,但是这个类具体是哪个,目前尚未可知。将鼠标放在ErrorAttributes接口上,右键然后按照图示操作进行:

可以发现它跳到了DefaultErrorAttributes类中,而这个类是有getErrorAttributes方法,所以最终调用的就是这里的getErrorAttributes方法。

其实DefaultErrorAttributes类是在ErrorMvcAutoConfiguration类中默认提供的。查看一下ErrorMvcAutoConfiguration类的errorAttributes方法:

1
2
3
4
5
6
7
8
@Bean
@ConditionalOnMissingBean(
value = {ErrorAttributes.class},
search = SearchStrategy.CURRENT
)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
}

从这段源码中可以看出,当系统没有提供ErrorAttributes时,它才会使用默认的DefaultErrorAttributes,所以自定义错误提示时,只需要提供一个ErrorAttributes即可,而DefaultErrorAttributesErrorAttributes的子类,因此只需要继承DefaultErrorAttributes即可,然后重写其中的getErrorAttributes方法即可。

新建一个component包,接着在里面新建一个MyErrorAttribute类,并继承DefaultErrorAttributes类,然后重写其中的getErrorAttributes方法。相应的代码为:

1
2
3
4
5
6
7
8
9
10
@Component
public class MyErrorAttribute extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String,Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
errorAttributes.put("customMsg","对不起,出错了");
errorAttributes.remove("error");
return errorAttributes;
}
}

解释一下上面代码的含义:首先自定义MyErrorAttribute类,并继承DefaultErrorAttributes类,然后重写其中的getErrorAttributes方法。请注意必须在自定义的MyErrorAttribute类上添加@Component注解,这样该类才会被注册到Spring容器中。接着通过super.getErrorAttributes()方法来获取到SpringBoot默认提供的错误信息,然后在此基础上添加Error信息,并移除默认的Error信息。

当系统抛出异常时,SpringBoot肯定会将信息输出到之前的动态页面模板中,以4xx.html模板为例进行修改,需要增加一行用于输出自定义的customMsg信息:

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
<!DOCTYPE html>
<html lang="en" xmlns="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>4xx</title>
</head>
<body>
<table border="1px">
<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>customMsg</td>
<td th:text="${customMsg}"></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>

5xx.html和这个几乎完全一致,这里就不再粘贴代码了。

运行项目,访问一个并不存在的链接,可以看到页面输出信息为:

且由于在自定义的MyErrorAttribute类中移除了error对象,因此页面不会有任何信息展示。

如果你使用Postman等测试工具来发起请求,那么返回的JSON数据也是如此:

2、自定义Error视图。Error视图是展示给用户的页面。前面说过BasicErrorController类的errorHtml方法会调用resolveErrorView方法去获取ModelAndView对象。而这个resolveErrorView方法其实来自于ErrorViewResolver接口:

下面就是ErrorViewResolver接口中的方法,它只有一个resolveErrorView方法,同样似乎没有完整的逻辑实现,使用上述方法来查一下它的实现类。

1
2
3
4
@FunctionalInterface
public interface ErrorViewResolver {
ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model);
}

找到了,是这个DefaultErrorViewResolver类。阅读ErrorMvcAutoConfiguration这个类可以发现,SpringBoot中默认采用的ErrorViewResolver就是DefaultErrorViewResolver。这里依然粘贴一下有关DefaultErrorViewResolver的代码:

1
2
3
4
@Bean   @ConditionalOnBean({DispatcherServlet.class})      @ConditionalOnMissingBean({ErrorViewResolver.class})
public DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}

看到这里似乎就豁然开朗了,ErrorMvcAutoConfiguration是一个与错误相关的自动配置类,SpringBoot中很多默认的信息就是这里来自动调用的。

在上面的代码中,如果用户没有提供ErrorViewResolver,那么默认使用DefaultErrorViewResolver。查看一下DefaultErrorViewResolver这个类:

其实又回到了前面的自定义Error数据,DefaultErrorViewResolver中配置了默认其error目录下寻找4xx.html和5xx.html文件。所以开发者想自定义Error视图,只需要提供自己的ErrorViewResolver即可。仿照前面的思路,由于DefaultErrorViewResolverErrorViewResolver的子类,因此只需要继承DefaultErrorViewResolver即可,然后重写其中的resolveErrorView方法。但是实际上DefaultErrorViewResolver类没有提供无参的构造方法,因此会影响到后续自定义MyErrorViewResolver类的实例化,所以这里推荐直接实现ErrorViewResolver接口。

在component包内新建一个MyErrorViewResolver类,并实现ErrorViewResolver接口,然后重写其中的resolveErrorView方法。相应的代码为:

1
2
3
4
5
6
7
8
9
10
11
@Component
public class MyErrorViewResolver implements ErrorViewResolver {
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("customMsg","出错啦");
modelAndView.setViewName("errorPage");
modelAndView.addAllObjects(model);
return modelAndView;
}
}

解释一下上面代码的含义:首先自定义MyErrorViewResolver类,并实现ErrorViewResolver接口,然后重写其中的resolveErrorView方法。请注意必须在自定义的MyErrorViewResolver类上添加@Component注解,这样该类才会被注册到Spring容器中。在resolveErrorView方法中,最后一个Map参数就是SpringBoot提供的默认5条Error信息(可以参照前面自定义Error的数据对这5条消息进行修改)。在resolveErrorView方法用于返回一个ModelAndView对象,可以在ModelAndView中设置Error视图和Error数据。所以开发者除了前面介绍的继承DefaultErrorAttributes类的方法外,还可以通过实现ErrorViewResolver接口来实现Error数据的自定义,当然如果仅仅是想自定义Error数据,还是建议继承DefaultErrorAttributes的方式来实现。

既然设置了错误视图为errorPage,那么就需要在resources/templates/目录下提供一个errorPage.html文件,里面的代码为:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>errorPage</title>
</head>
<body>
<table border="1px">
<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>customMsg</td>
<td th:text="${customMsg}"></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>

errorPage.html文件中除了展示SpringBoot提供的5条Error信息外,也展示了开发者自定义的Error信息(customMsg)。此时无论请求发送4xx错误还是5xx的错误,页面都会显示errorPage.html页面:

3、完全自定义。通过前两种方式的介绍,其实可以猜得到所谓的完全自定义就是脱离BasicErrorController这个类,原因在于前面两种方式就是对BasicErrorController类中的某个部分进行修改实现自定义。为了验证猜想,可以查看ErrorMvcAutoConfiguration这个错误自动配置类,只要里面有类似于前面的代码说明这个BasicErrorController类是缺省的,是默认由SpringBoot提供的即可:

1
2
3
4
5
6
7
8
@Bean
@ConditionalOnMissingBean(
value = {ErrorController.class},
search = SearchStrategy.CURRENT
)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
}

果不其然,真的发现了。如果开发者没有提供自己的ErrorController,那么SpringBoot将提供BasicErrorController作为默认的ErrorController。所以如果开发者想更加灵活的对Error视图和Error数据进行定制,只需要提供自己的ErrorController即可。提供自己的ErrorController有两种方法:第一种实现ErrorController接口,另一种就是直接继承BasicErrorController。由于ErrorController接口中只提供了一个待实现的方法,而BasicErrorController类已经实现了很多功能,因此这里考虑第二种方式。查看一下这个BasicErrorController类是否提供了无参的构造方法,你会发现没有提供,所以如果你还想使用这种方法,就必须在子类的构造方法中显式地调用父类有参的构造方法即可。在controller包中新建一个MyErrorController类,里面的代码为:

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
@Controller
public class MyErrorController extends BasicErrorController {

@Autowired
public MyErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties, List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, serverProperties.getError(), 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("customMsg","对不起,出错啦");
ModelAndView modelAndView = new ModelAndView("myErrorPage",model,status);
return modelAndView;

}

@Override
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
body.put("customMsg","对不起,出错啦");
return new ResponseEntity(body, status);
}
}
}

解释一下上面代码的含义:首先自定义的MyErrorController是一个Controller,因此需要添加@Controller注解,用于将MyErrorController注册到SpringMVC容器中。其次由于BasicErrorController类没有提供无参的构造方法,因此在创建BasicErrorController实例时需要传入参数,在MyErrorController类的构造方法上添加@Autowired注解来自动注入所需参数。最后参考BasicErrorController类中的实现,来重写errorHtml和error方法,来对Error视图和数据进行完全的自定义。

既然设置了错误视图为myErrorPage,那么就需要在resources/templates/目录下提供一个myErrorPage.html文件,里面的代码为:

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
<!DOCTYPE html>
<html lang="en" xmlns="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>myErrorPage</title>
</head>
<body>
<table border="1px">
<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>customMsg</td>
<td th:text="${customMsg}"></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>

myErrorPage.html文件中除了展示SpringBoot提供的5条Error信息外,也展示了开发者自定义的Error信息(customMsg)。此时无论请求发送4xx错误还是5xx的错误,页面都会显示myErrorPage.html页面:

如果你使用Postman等测试工具来发起请求,那么返回的就是一段JSON数据:

SpringBoot对于异常的处理还是较为简单的,它虽然提供了丰富的自动化配置方案,但是也允许开发者根据实际情况进行完全、半完全化的定制,所以开发者可以结合自己的需求来选择使用合适的Error处理方案。如此看来SpringBoot中的XXXMvcAutoConfiguration类中允许用户传入自己的配置信息,也可以不传使用缺省的配置信息,这就使得开发变得较为灵活。

CORS支持

CORS(CrossOriginResourceSharing)是一种跨域资源共享,用来解决前端的跨域请求。在javaEE开发中,最常见的前端跨域解决方案是JSONP,但是JSONP只支持GET请求,这是一个非常大的缺陷,而CORS则支持多种HTTP请求方法。以CORS中的GET请求为例,当浏览器发起请求时,请求头中携带了如下信息:

1
2
3
4
5
......
Host: localhost:8080
Origin: http://localhost:8081
Referer: http://localhost:8081/index.html
......

假设服务端支持CORS,则服务端给出的响应信息为:

1
2
3
4
5
6
......
Access-Control-Allow-Origin: http://localhost:8081
Content-Length: 20
Content-Type: text/plain;charset=UTF-8
Date: Wed, 13 May 2019 01:26:41 GMT
......

注意,响应头中有一个Access-Control-Allow-Origin字段,用来记录可以访问该资源的域。当浏览器收到这样的响应头信息后,提取出Access-Control-Allow-Origin字段中的值,发现该值包含当前页面所在的域,就知道这个跨域是被允许的,因此举不再对前端的跨域请求进行限制。这就是GET请求的整个跨域流程,在这个过程中前端请求的代码不需要修改,主要是后端进行处理。这个流程主要是针对GET、POST、以及HEAD请求,且没有自定义请求头,如果用户发起一个DELETE请求,PUT请求或者自定义了请求头,那么这个过程就显得稍微复杂一些。

以DELETE为例,当前端发起一个DELETE请求时,这个请求的处理会经过两个步骤。第一步:发送一个OPTIONS请求,代码为:

1
2
3
4
5
6
......
Access-Control-Request-Method DELETE
Connection keep-alive
Host localhost:8080
Origin: http://localhost:8081
......

这个请求将向服务端询问是否具备该资源的DELETE权限,服务端会给浏览器一个响应,代码为:

1
2
3
4
5
6
7
8
9
......
HTTP/1.1 200
Access-Control-Allow-Origin: http://localhost:8081
Access-Control-Allow-Methods: DELETE
Access-Control-Max-Age: 1800
Allow: GET,HEAD,POST,PUT,DELETE,OPTIONS,PATCH
Content-Length: 0
Date: Wed, 13 May 2019 01:28:41 GMT
......

上面是服务端给浏览器的响应,Allow头信息表示服务端支持的请求方法,可以发现这是标准的RESTful风格,这个请求相当于一个探测请求,当浏览器分析了请求字段后,知道服务端支持本次请求,则进入第二步。第二步:发送DELETE请求。接下来浏览器就会发送一个跨域的DELETE请求,代码为:

1
2
3
4
5
......
Host localhost:8080
Origin: http://localhost:8081
Connection keep-alive
......

这时候服务端就会给它一个响应:

1
2
3
4
HTTP/1.1 200
Access-Control-Allow-Origin: http://localhost:8081
Content-Type: text/plain;charset=UTF-8
Date: Wed, 13 May 2019 01:29:41 GMT

这样一个跨域的DELETE请求就完成了。

无论是简单的请求还是需要先进行探测的请求,前端的写法都是不变的,额外的处理都是在服务器端完成的。在传统的javaEE开发中,可以通过过滤器统一配置,而SpringBoot中对此进行了简化,提供了更简洁的解决方案。在SpringBoot中配置CORS的步骤如下:

第一步,创建SpringBoot工程。使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为corsspringboot,然后添加spring-boot-starter-web依赖。

第二步,创建控制器。com.envy.corsspringboot包内新建一个controller包,接着在里面新建BookController控制类,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/book")
public class BookController {

/**增加书籍**/
@PostMapping("/")
public String addBook(String name){
return "receive: "+name;
}

/**删除书籍**/
@PostMapping("/{id}")
public String deleteBookById(@PathVariable("id")Long id){
return String.valueOf(id);
}
}

这个BookController类中提供了两个接口,一个是增加书籍,另一个是删除书籍。

第三步,配置跨域。跨域有两个地方可以配置。第一个是直接在相应的方法上添加注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/book")
public class BookController {

/**增加书籍**/
@PostMapping("/")
@CrossOrigin(value = "http://localhost:8081",maxAge = 1800,allowedHeaders = "*")
public String addBook(String name){
return "receive: "+name;
}

/**删除书籍**/
@DeleteMapping("/{id}")
@CrossOrigin(value = "http://localhost:8081",maxAge = 1800,allowedHeaders = "*")
public String deleteBookById(@PathVariable("id")Long id){
return String.valueOf(id);
}
}

仅仅在每个方法上添加一行代码:@CrossOrigin(value = "http://localhost:8081",maxAge = 1800,allowedHeaders = "*")就完成了服务器端的跨域设置。查看@CrossOrigin注解源码可知它是一个接口注解:

其中的value表示支持的域,这里表示来自http://localhost:8081域的请求时支持跨域的。maxAge表示探测请求的有效期。前面说过,对于DELETE和PUT请求或者有自定义头信息的请求,在执行过程中会先发送探测请求,探测请求不需要每次都发送,可以设一个有效期,只有有效期过了以后才会发送探测请求。这个属性默认值为1800秒,也就是30分钟。后面的allowedHeaders表示允许的请求头,*显然表示所有的请求头都被允许。

上面这种配置方式颗粒度非常细,是在每个方法上进行了配置,可以控制到每个方法,自然就相对来说较为复杂一些。

其实也可以不在每个方法上添加CrossOrigin注解,而是采用全局配置的方式,这就是第二种方法。新建一个config包,接着在里面新建一个MyWebMvcConfig类,让它实现WebMvcConfigurer接口。这个WebMvcConfigurer接口非常有用,里面有很多方法,像addViewControllersaddResourceHandlersaddCorsMappingsconfigureViewResolvers等都是经常可以使用到的方法。自定义MyWebMvcConfig类中的代码为:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/book/**")
.allowedHeaders("*")
.allowedMethods("*")
.maxAge(1800)
.allowedOrigins("http://localhost:8081");
}
}

解释一下上述代码的含义:首先自定义的MyWebMvcConfig类需要实现WebMvcConfigurer接口,且实现其中的addCorsMappings方法。在addCorsMappings方法中,addMapping表示对哪种格式的请求路径进行跨域处理;allowedHeaders表示允许的请求头,默认允许所有的请求头信息;allowedMethods表示允许的请求方法,默认是GET、POST和HEAD,*表示支持所有的请求方法;maxAge表示探测请求的有效期;allowedOrigins表示支持的域,其实也就是前端的IP地址及端口信息。

上面介绍了两种配置方式(@CrossOrigin注解和全局配置),在实际开发过程中需要结合具体情况进行分析,并选择一种即可。然后启动项目。

第四步、测试。再次创建SpringBoot工程,使其模拟前端客户端。使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为testspringboot,然后添加spring-boot-starter-web依赖。(我这里使用了前面的filespringboot项目)。

首先需要在application.properties配置文件中将端口设置为8081,:server.port=8081因为前面设置了允许域为http://localhost:8081

接着在resources/static目录下加入jquery.js文件,同时新建一个index.html文件,里面的代码为:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>前端跨域</title>
<script type="text/javascript" src="jquery-3.4.1.js"></script>
</head>
<body>
<h2 >前端跨域测试</h2>
<div id="contentDiv"></div>
<div id="deleteResult"></div>
<div>
<input type="button" value="提交数据" id="submitData"><br>
<input type="button" value="删除数据" id="deleteData"><br>
</div>

<script type="text/javascript">
$(document).read(
$('#submitData').click(function () {
$.ajax({
url: "http://localhost:8080/book/",
type: 'post',
data: {
name: '西游记'
},
success: function (msg) {
$("#contentDiv").html(msg);
}
})
}),
$('#deleteData').click(function () {
$.ajax({
url: 'http://localhost:8080/book/66',
type: 'delete',
success: function (msg) {
$('#deleteResult').html(msg)
}
})
})
)
</script>
</body>
</html>

这个html文件中的代码非常简单,这里就不细说了,它们都采用ajax方法向后端获取数据。

运行项目,在浏览器地址栏中输入http://localhost:8081/index.html,查看页面然后分别单击两个按钮查看请求结果。请注意必须启动前面的corsspringboot项目,因为它就代表着服务器:

配置类与XML配置

SpringBoot推荐使用Java来完成相关的配置工作。在项目中,不建议将所有的配置放在一个配置类中,这违反了单一职责原则,可以根据不同的需求提供不同的配置类,如专门处理SpringSecurity的配置类、提供Bean的配置类、SpringMVC相关的配置类。这些配置类上都需要添加@Configuration注解,@ComponentScan注解会扫描所有的Spring组件,也包括@Configuration。在一开始就曾介绍过,@ComponentScan注解在项目入口类的@SpringBootApplication注解中已经提供,因此在实际项目中只需要按照需要提供相关的配置类即可。

SpringBoot中并不推荐使用XML配置,而是尽量使用Java配置来代替XML配置,我个人也是比较赞成这个观点。这里既然是学习,可以了解如何在SpringBoot中使用XML配置,只需要在resources目录下提供配置文件,然后通过使用@ImportResource注解指定XML配置文件位置的方式来加载配置文件即可。

corsspringboot项目中新建一个pojo包,并在里面新建一个Envy类,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Envy {
private String name;

public String getName(){
return name;
}

public void setName(String name){
this.name=name;
}

public String hello(String name){
return "hello,"+name;
}
}

接着在resources目录下新建一个beans.xml文件:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean class="com.envy.corsspringboot.pojo.Envy" id="hello"></bean>
</beans>

其实这就是Spring中对于Bean的手动管理方式。接着在config包内,新建一个Beans的配置类,用于导入XML的配置:

1
2
3
4
@Configuration
@ImportResource("classpath:bean.xml")
public class Beans {
}

然后在Controller包中新建一个EnvyController类,里面直接导入Envy类就可以使用了:

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class EnvyController {

@Autowired
private Envy envy;

@GetMapping("/hello")
public String hello(){
return envy.hello("余思博客");
}
}

这样其实显得非常鸡肋,完全可以在定义的Envy类上添加@Component注解就能实现同样的功能,所以在SprigBoot中还是使用java配置更方便一些。

注册拦截器

SpringMVC提供了AOP方式的拦截器,使其拥有更加精细的拦截处理能力。而SpringBoot中拦截器的注册对其进行了进一步简化,使得它使用变得更加简单。SpringBoot中拦截器的使用步骤如下:

第一步,创建SpringBoot项目。新建一个SpringBoot的Web的应用。(我这里依旧使用前面的corsspringboot项目)

第二步,创建实现拦截器接口的实现类。新建一个handle包,并在里面新建一个MyInterceptor类。自定义的拦截器必须实现HandleInterceptor接口,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("this is MyInterceptor's >>>> preHandle");
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("this is MyInterceptor's >>>> postHandle");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("this is MyInterceptor's >>>> afterCompletion");
}
}

拦截器中的方法将按照preHandle-->Controller-->postHandle-->afterCompletion的顺序执行。请注意只有preHandle这个方法返回true时,后面的方法才会执行。当拦截器链内存在多个拦截器时,postHandle在拦截器链内的所有拦截器返回成功时才会被调用。而afterCompletion只有preHandle这个方法返回true时才调用,但若拦截器链内的第一个拦截器的preHandle方法返回false时,则后面的方法都不会执行。关于拦截器相关的知识可以阅读《SpringMVC学习(4):SpringMVC拦截器》这篇文章。

第三步,配置拦截器规则。接下来就是配置拦截器的拦截规则,你需要定义一个配置类MyHandleWebMvcConfig,并实现WebMvcConfigurer接口,且重写其中的addInterceptors方法,这个在CORS中已经有提到过。里面的代码为:

1
2
3
4
5
6
7
8
9
@Configuration
public class MyHandleWebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/hello");
}
}

上述代码中的addInterceptor方法需要传入一个HandlerInterceptor对象,而前面自定义的MyInterceptor类实现了HandlerInterceptor接口,因此可以将其传入,addPathPatterns表示拦截路径,excludePathPatterns表示排除的路径,也就是不拦截的路径。这样方式比SpringMVC中在springmvc.xml手动注册拦截器明显方便一些。

第四步,测试。运行项目,在浏览器地址栏中分别输入:http://localhost:8080/hellohttp://localhost:8080/hellotwo,当你访问后者时,由于被拦截器拦截,因此控制台会输出以下信息(如果出现多次执行情况,可能与之前的配置有关):

1
2
3
this is MyInterceptor's >>>> preHandle
this is MyInterceptor's >>>> postHandle
this is MyInterceptor's >>>> afterCompletion