SpringBoot Web开发整合(中)
本篇是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 | @Controller |
其中的errorHtml方法用来返回错误的HTML页面,error方法用来返回错误JOSN,因此具体返回的是HTML还是JSON需要看请求头的Accept参数,其实就是MediaType
的类型。返回JSON的逻辑很简单,一眼就能看懂,关键是返回HTML的逻辑稍微有些复杂,因为涉及到视图解析器。
在errorHtml方法中,通过调用resolveErrorView
方法来获取一个错误视图ModelAndView。而resolveErrorView
方法的调用最终会来到DefaultErrorViewResolver
类中。DefaultErrorViewResolver
类是SpringBoot中默认的错误信息视图解析器,可以阅读其源码,这里粘贴部分源码:
1 | public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { |
从上面的源码可以看出,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 | // resources\static\error\404.html |
启动项目,当用户访问一个不存在的路径时,刚才定义的404页面就会被展示出来了:
4xx是客户端的错误,5xx是服务器的错误,也就是java程序的错误,因此可以创建一个controller
包,并在里面新建一个TestController
,里面定义一个test方法,该方法只要抛出一个异常即可,最简单的就是算数除法除数为0了:
1 | @RestController |
运行项目,访问该链接即可将刚才定义的500页面展示出来:
前面使用的都是静态HTML页面,无法向用户展示完整的错误信息,如果想展示更多的信息,可以使用视图模板技术。如果想使用HTML模板,那么需要在pom.xml中引入模板的相关依赖,这里以Thymeleaf
为例进行说明。
在pom.xml中导入Thymeleaf
依赖:
1 | <!--添加Thymeleaf依赖--> |
请注意Thymeleaf
页面模板默认处于classpath:/templates/
目录下,因此需要在该目录下先创建error目录,再创建错误展示页,如图所示:
由于模板页面信息展示较为灵活,因此只需要创建4xx.html和5xx.html文件即可。以4xx.html为例说一下其中的内容:
1 | <!DOCTYPE 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 | protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) { |
继续点进去查看,找到了private final ErrorAttributes errorAttributes;
再点进去查看,发现这个ErrorAttributes
是一个接口:
1 | public interface ErrorAttributes { |
这个接口中虽然有getErrorAttributes
方法,但是它只需传入webRequest
和includeStackTrace
对象,但是includeStackTrace
对象具体是什么,这个也没有指明,说明此处的getErrorAttributes
方法在其他的类中存在不同方式的实现,但是这个类具体是哪个,目前尚未可知。将鼠标放在ErrorAttributes
接口上,右键然后按照图示操作进行:
可以发现它跳到了DefaultErrorAttributes
类中,而这个类是有getErrorAttributes
方法,所以最终调用的就是这里的getErrorAttributes
方法。
其实DefaultErrorAttributes
类是在ErrorMvcAutoConfiguration
类中默认提供的。查看一下ErrorMvcAutoConfiguration
类的errorAttributes
方法:
1 | @Bean |
从这段源码中可以看出,当系统没有提供ErrorAttributes
时,它才会使用默认的DefaultErrorAttributes
,所以自定义错误提示时,只需要提供一个ErrorAttributes
即可,而DefaultErrorAttributes
是ErrorAttributes
的子类,因此只需要继承DefaultErrorAttributes
即可,然后重写其中的getErrorAttributes
方法即可。
新建一个component包,接着在里面新建一个MyErrorAttribute
类,并继承DefaultErrorAttributes
类,然后重写其中的getErrorAttributes
方法。相应的代码为:
1 | @Component |
解释一下上面代码的含义:首先自定义MyErrorAttribute
类,并继承DefaultErrorAttributes
类,然后重写其中的getErrorAttributes
方法。请注意必须在自定义的MyErrorAttribute
类上添加@Component
注解,这样该类才会被注册到Spring容器中。接着通过super.getErrorAttributes()
方法来获取到SpringBoot默认提供的错误信息,然后在此基础上添加Error信息,并移除默认的Error信息。
当系统抛出异常时,SpringBoot肯定会将信息输出到之前的动态页面模板中,以4xx.html模板为例进行修改,需要增加一行用于输出自定义的customMsg
信息:
1 | <!DOCTYPE html> |
5xx.html和这个几乎完全一致,这里就不再粘贴代码了。
运行项目,访问一个并不存在的链接,可以看到页面输出信息为:
且由于在自定义的MyErrorAttribute
类中移除了error对象,因此页面不会有任何信息展示。
如果你使用Postman等测试工具来发起请求,那么返回的JSON数据也是如此:
2、自定义Error视图。Error视图是展示给用户的页面。前面说过BasicErrorController
类的errorHtml方法会调用resolveErrorView
方法去获取ModelAndView对象。而这个resolveErrorView
方法其实来自于ErrorViewResolver
接口:
下面就是ErrorViewResolver
接口中的方法,它只有一个resolveErrorView
方法,同样似乎没有完整的逻辑实现,使用上述方法来查一下它的实现类。
1 | @FunctionalInterface |
找到了,是这个DefaultErrorViewResolver
类。阅读ErrorMvcAutoConfiguration
这个类可以发现,SpringBoot中默认采用的ErrorViewResolver
就是DefaultErrorViewResolver
。这里依然粘贴一下有关DefaultErrorViewResolver
的代码:
1 | @Bean @ConditionalOnBean({DispatcherServlet.class}) @ConditionalOnMissingBean({ErrorViewResolver.class}) |
看到这里似乎就豁然开朗了,ErrorMvcAutoConfiguration
是一个与错误相关的自动配置类,SpringBoot中很多默认的信息就是这里来自动调用的。
在上面的代码中,如果用户没有提供ErrorViewResolver
,那么默认使用DefaultErrorViewResolver
。查看一下DefaultErrorViewResolver
这个类:
其实又回到了前面的自定义Error数据,DefaultErrorViewResolver
中配置了默认其error目录下寻找4xx.html和5xx.html文件。所以开发者想自定义Error视图,只需要提供自己的ErrorViewResolver
即可。仿照前面的思路,由于DefaultErrorViewResolver
是ErrorViewResolver
的子类,因此只需要继承DefaultErrorViewResolver
即可,然后重写其中的resolveErrorView
方法。但是实际上DefaultErrorViewResolver
类没有提供无参的构造方法,因此会影响到后续自定义MyErrorViewResolver
类的实例化,所以这里推荐直接实现ErrorViewResolver
接口。
在component包内新建一个MyErrorViewResolver
类,并实现ErrorViewResolver
接口,然后重写其中的resolveErrorView
方法。相应的代码为:
1 | @Component |
解释一下上面代码的含义:首先自定义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 | <!DOCTYPE html> |
在errorPage.html
文件中除了展示SpringBoot提供的5条Error信息外,也展示了开发者自定义的Error信息(customMsg)。此时无论请求发送4xx错误还是5xx的错误,页面都会显示errorPage.html
页面:
3、完全自定义。通过前两种方式的介绍,其实可以猜得到所谓的完全自定义就是脱离BasicErrorController
这个类,原因在于前面两种方式就是对BasicErrorController
类中的某个部分进行修改实现自定义。为了验证猜想,可以查看ErrorMvcAutoConfiguration
这个错误自动配置类,只要里面有类似于前面的代码说明这个BasicErrorController
类是缺省的,是默认由SpringBoot提供的即可:
1 | @Bean |
果不其然,真的发现了。如果开发者没有提供自己的ErrorController
,那么SpringBoot将提供BasicErrorController
作为默认的ErrorController
。所以如果开发者想更加灵活的对Error视图和Error数据进行定制,只需要提供自己的ErrorController
即可。提供自己的ErrorController
有两种方法:第一种实现ErrorController
接口,另一种就是直接继承BasicErrorController
。由于ErrorController
接口中只提供了一个待实现的方法,而BasicErrorController
类已经实现了很多功能,因此这里考虑第二种方式。查看一下这个BasicErrorController
类是否提供了无参的构造方法,你会发现没有提供,所以如果你还想使用这种方法,就必须在子类的构造方法中显式地调用父类有参的构造方法即可。在controller包中新建一个MyErrorController
类,里面的代码为:
1 | @Controller |
解释一下上面代码的含义:首先自定义的MyErrorController
是一个Controller,因此需要添加@Controller
注解,用于将MyErrorController
注册到SpringMVC容器中。其次由于BasicErrorController
类没有提供无参的构造方法,因此在创建BasicErrorController
实例时需要传入参数,在MyErrorController
类的构造方法上添加@Autowired
注解来自动注入所需参数。最后参考BasicErrorController
类中的实现,来重写errorHtml和error方法,来对Error视图和数据进行完全的自定义。
既然设置了错误视图为myErrorPage
,那么就需要在resources/templates/
目录下提供一个myErrorPage.html
文件,里面的代码为:
1 | <!DOCTYPE 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 | ...... |
假设服务端支持CORS,则服务端给出的响应信息为:
1 | ...... |
注意,响应头中有一个Access-Control-Allow-Origin
字段,用来记录可以访问该资源的域。当浏览器收到这样的响应头信息后,提取出Access-Control-Allow-Origin
字段中的值,发现该值包含当前页面所在的域,就知道这个跨域是被允许的,因此举不再对前端的跨域请求进行限制。这就是GET请求的整个跨域流程,在这个过程中前端请求的代码不需要修改,主要是后端进行处理。这个流程主要是针对GET、POST、以及HEAD请求,且没有自定义请求头,如果用户发起一个DELETE请求,PUT请求或者自定义了请求头,那么这个过程就显得稍微复杂一些。
以DELETE为例,当前端发起一个DELETE请求时,这个请求的处理会经过两个步骤。第一步:发送一个OPTIONS请求,代码为:
1 | ...... |
这个请求将向服务端询问是否具备该资源的DELETE权限,服务端会给浏览器一个响应,代码为:
1 | ...... |
上面是服务端给浏览器的响应,Allow头信息表示服务端支持的请求方法,可以发现这是标准的RESTful风格,这个请求相当于一个探测请求,当浏览器分析了请求字段后,知道服务端支持本次请求,则进入第二步。第二步:发送DELETE请求。接下来浏览器就会发送一个跨域的DELETE请求,代码为:
1 | ...... |
这时候服务端就会给它一个响应:
1 | HTTP/1.1 200 |
这样一个跨域的DELETE请求就完成了。
无论是简单的请求还是需要先进行探测的请求,前端的写法都是不变的,额外的处理都是在服务器端完成的。在传统的javaEE开发中,可以通过过滤器统一配置,而SpringBoot中对此进行了简化,提供了更简洁的解决方案。在SpringBoot中配置CORS的步骤如下:
第一步,创建SpringBoot工程。使用spring Initializr
构建工具构建一个SpringBoot的Web应用,名称为corsspringboot
,然后添加spring-boot-starter-web
依赖。
第二步,创建控制器。在com.envy.corsspringboot
包内新建一个controller包,接着在里面新建BookController控制类,里面的代码为:
1 | @RestController |
这个BookController类中提供了两个接口,一个是增加书籍,另一个是删除书籍。
第三步,配置跨域。跨域有两个地方可以配置。第一个是直接在相应的方法上添加注解:
1 | @RestController |
仅仅在每个方法上添加一行代码:@CrossOrigin(value = "http://localhost:8081",maxAge = 1800,allowedHeaders = "*")
就完成了服务器端的跨域设置。查看@CrossOrigin
注解源码可知它是一个接口注解:
其中的value表示支持的域,这里表示来自http://localhost:8081
域的请求时支持跨域的。maxAge表示探测请求的有效期。前面说过,对于DELETE和PUT请求或者有自定义头信息的请求,在执行过程中会先发送探测请求,探测请求不需要每次都发送,可以设一个有效期,只有有效期过了以后才会发送探测请求。这个属性默认值为1800秒,也就是30分钟。后面的allowedHeaders
表示允许的请求头,*显然表示所有的请求头都被允许。
上面这种配置方式颗粒度非常细,是在每个方法上进行了配置,可以控制到每个方法,自然就相对来说较为复杂一些。
其实也可以不在每个方法上添加CrossOrigin
注解,而是采用全局配置的方式,这就是第二种方法。新建一个config包,接着在里面新建一个MyWebMvcConfig
类,让它实现WebMvcConfigurer
接口。这个WebMvcConfigurer
接口非常有用,里面有很多方法,像addViewControllers
、addResourceHandlers
、addCorsMappings
、configureViewResolvers
等都是经常可以使用到的方法。自定义MyWebMvcConfig
类中的代码为:
1 | @Configuration |
解释一下上述代码的含义:首先自定义的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 | <!DOCTYPE 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 | public class Envy { |
接着在resources目录下新建一个beans.xml
文件:
1 | <?xml version="1.0" encoding="UTF-8"?> |
其实这就是Spring中对于Bean的手动管理方式。接着在config包内,新建一个Beans的配置类,用于导入XML的配置:
1 | @Configuration |
然后在Controller包中新建一个EnvyController
类,里面直接导入Envy类就可以使用了:
1 | @RestController |
这样其实显得非常鸡肋,完全可以在定义的Envy类上添加@Component
注解就能实现同样的功能,所以在SprigBoot中还是使用java配置更方便一些。
注册拦截器
SpringMVC提供了AOP方式的拦截器,使其拥有更加精细的拦截处理能力。而SpringBoot中拦截器的注册对其进行了进一步简化,使得它使用变得更加简单。SpringBoot中拦截器的使用步骤如下:
第一步,创建SpringBoot项目。新建一个SpringBoot的Web的应用。(我这里依旧使用前面的corsspringboot
项目)
第二步,创建实现拦截器接口的实现类。新建一个handle包,并在里面新建一个MyInterceptor
类。自定义的拦截器必须实现HandleInterceptor
接口,里面的代码为:
1 | public class MyInterceptor implements HandlerInterceptor { |
拦截器中的方法将按照preHandle-->Controller-->postHandle-->afterCompletion
的顺序执行。请注意只有preHandle
这个方法返回true时,后面的方法才会执行。当拦截器链内存在多个拦截器时,postHandle
在拦截器链内的所有拦截器返回成功时才会被调用。而afterCompletion
只有preHandle
这个方法返回true时才调用,但若拦截器链内的第一个拦截器的preHandle
方法返回false时,则后面的方法都不会执行。关于拦截器相关的知识可以阅读《SpringMVC学习(4):SpringMVC拦截器》这篇文章。
第三步,配置拦截器规则。接下来就是配置拦截器的拦截规则,你需要定义一个配置类MyHandleWebMvcConfig
,并实现WebMvcConfigurer
接口,且重写其中的addInterceptors
方法,这个在CORS中已经有提到过。里面的代码为:
1 | @Configuration |
上述代码中的addInterceptor
方法需要传入一个HandlerInterceptor
对象,而前面自定义的MyInterceptor
类实现了HandlerInterceptor
接口,因此可以将其传入,addPathPatterns
表示拦截路径,excludePathPatterns
表示排除的路径,也就是不拦截的路径。这样方式比SpringMVC中在springmvc.xml
手动注册拦截器明显方便一些。
第四步,测试。运行项目,在浏览器地址栏中分别输入:http://localhost:8080/hello
和http://localhost:8080/hellotwo
,当你访问后者时,由于被拦截器拦截,因此控制台会输出以下信息(如果出现多次执行情况,可能与之前的配置有关):
1 | this is MyInterceptor's >>>> preHandle |