SpringBoot對(duì)Spring MVC都做了哪些事?(三)
上一篇:《??SpringBoot對(duì)Spring MVC都做了哪些事?(二)??》
錯(cuò)誤處理
默認(rèn)情況下,Spring Boot提供了一個(gè)/error映射,以合理的方式處理所有錯(cuò)誤,并在servlet容器中注冊(cè)為“全局”錯(cuò)誤頁(yè)。對(duì)于機(jī)器客戶(hù)端,它生成一個(gè)JSON響應(yīng),其中包含錯(cuò)誤、HTTP狀態(tài)和異常消息的詳細(xì)信息。對(duì)于瀏覽器客戶(hù)端,有一個(gè)以HTML格式呈現(xiàn)相同數(shù)據(jù)的“whitelabel”錯(cuò)誤視圖(要對(duì)其進(jìn)行自定義,請(qǐng)?zhí)砑右粋€(gè)解決錯(cuò)誤的視圖)。
如果要自定義默認(rèn)錯(cuò)誤處理行為,可以設(shè)置許多server.error屬性。
要完全替換默認(rèn)行為,可以實(shí)現(xiàn)ErrorController并注冊(cè)該類(lèi)型的bean定義,或者添加ErrorAttributes類(lèi)型的bean以使用現(xiàn)有機(jī)制,但替換內(nèi)容。
你還可以定義一個(gè)用@ControllerAdvice注釋的類(lèi),以自定義JSON格式,以針對(duì)特定控制器和/或異常類(lèi)型返回,如以下示例所示:
basePackageClasses = AcmeController.class)public class AcmeControllerAdvice extends ResponseEntityExceptionHandler { (YourException.class) ResponseEntity<?> handleControllerException(HttpServletRequest request, Throwable ex) { HttpStatus status = getStatus(request); return new ResponseEntity<>(new CustomErrorType(status.value(), ex.getMessage()), status); } private HttpStatus getStatus(HttpServletRequest request) { Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); if (statusCode == null) { return HttpStatus.INTERNAL_SERVER_ERROR; } return HttpStatus.valueOf(statusCode); }}(
在前面的示例中,如果你的異常是由與AcmeController在同一個(gè)包中定義的控制器引發(fā)的,那么將使用CustomErrorType POJO的JSON表示,而不是ErrorAttributes表示。
自定義錯(cuò)誤頁(yè)
如果要顯示給定狀態(tài)代碼的自定義HTML錯(cuò)誤頁(yè)面,可以將文件添加到/error目錄。錯(cuò)誤頁(yè)面可以是靜態(tài)HTML(即,添加到任何靜態(tài)資源目錄下),也可以使用模板構(gòu)建。文件名應(yīng)為準(zhǔn)確的狀態(tài)代碼或序列掩碼。
例如,要將404映射到靜態(tài)HTML文件,目錄結(jié)構(gòu)如下:
src/ +- main/ +- java/ | + <source code> +- resources/ +- public/ +- error/ | +- 404.html +- <other public assets>
要使用FreeMarker模板映射所有5xx錯(cuò)誤,目錄結(jié)構(gòu)如下:
src/ +- main/ +- java/ | + <source code> +- resources/ +- templates/ +- error/ | +- 5xx.ftlh +- <other templates>
對(duì)于更復(fù)雜的映射,你還可以添加實(shí)現(xiàn)ErrorViewResolver接口的bean,如以下示例所示:
public class MyErrorViewResolver implements ErrorViewResolver { public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { // Use the request or status to optionally return a ModelAndView return ... }}
系統(tǒng)默認(rèn)提供的DefaultErrorViewResolver 在該類(lèi)中我們可以看到默認(rèn)從如下幾個(gè)位置查找錯(cuò)誤頁(yè)。
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);} ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { // 默認(rèn)進(jìn)入這里,根據(jù)錯(cuò)誤碼的序列進(jìn)行解析視圖 modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView;} private ModelAndView resolve(String viewName, Map<String, Object> model) { String errorViewName = "error/" + viewName; // ... return resolveResource(errorViewName, model); } private ModelAndView resolveResource(String viewName, Map<String, Object> model) { // 從以下路徑進(jìn)行查找(路徑都會(huì)拼接上error/目錄) // classpath:/META-INF/resources/ // classpath:/resources/ // classpath:/static/ // classpath:/public/ // 以上路徑都可以通過(guò)spring.web.resources.staticLocations進(jìn)行配置 for (String location : this.resources.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); if (resource.exists()) { return new ModelAndView(new HtmlResourceView(resource), model); } } } return null; }}
Spring MVC之外的映射錯(cuò)誤頁(yè)面
對(duì)于不使用Spring MVC的應(yīng)用程序,可以使用ErrorPageRegistrar接口直接注冊(cè)ErrorPages。此抽象直接與底層嵌入式servlet容器一起工作,即使你沒(méi)有Spring MVC DispatcherServlet,也可以工作。
自定義錯(cuò)誤頁(yè)
ErrorPageRegistrar errorPageRegistrar(){ return new MyErrorPageRegistrar();}private static class MyErrorPageRegistrar implements ErrorPageRegistrar { public void registerErrorPages(ErrorPageRegistry registry) { registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400")); }}
工作原理
- TomcatServletWebServerFactory
public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware { private Set<ErrorPage> errorPages = new LinkedHashSet<>(); public WebServer getWebServer(ServletContextInitializer... initializers) { prepareContext(tomcat.getHost(), initializers); } protected void prepareContext(Host host, ServletContextInitializer[] initializers) { configureContext(context, initializersToUse); } protected void configureContext(Context context, ServletContextInitializer[] initializers) { TomcatStarter starter = new TomcatStarter(initializers); // 遍歷所有的ErrorPage對(duì)象,將其注冊(cè)到Tomcat容器中 for (ErrorPage errorPage : getErrorPages()) { org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage(); tomcatErrorPage.setLocation(errorPage.getPath()); tomcatErrorPage.setErrorCode(errorPage.getStatusCode()); tomcatErrorPage.setExceptionType(errorPage.getExceptionName()); context.addErrorPage(tomcatErrorPage); } }}
- 注冊(cè)處理器
// 在該自動(dòng)配置類(lèi)中,會(huì)注冊(cè)一個(gè)BeanPostProcessorsRegistrar處理器@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,...})public class ServletWebServerFactoryAutoConfiguration {}
- 注冊(cè)錯(cuò)誤頁(yè)注冊(cè)器
在上一步中注冊(cè)了BeanPostProcessorsRegistrar處理器。在該處理器中會(huì)注冊(cè)一個(gè)ErrorPageRegistrarBeanPostProcessor錯(cuò)誤頁(yè)注冊(cè)器的處理器。
public static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware { public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // ... // 注冊(cè)ErrorPageRegistrarBeanPostProcessor,又是一個(gè)處理器 registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor", ErrorPageRegistrarBeanPostProcessor.class, ErrorPageRegistrarBeanPostProcessor::new); }}
- 注冊(cè)錯(cuò)誤頁(yè)
public class ErrorPageRegistrarBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware { private ListableBeanFactory beanFactory; private List<ErrorPageRegistrar> registrars; public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = (ListableBeanFactory) beanFactory; } public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { // 判斷當(dāng)前的Bean是不是ErrorPageRegistry對(duì)象,在9.2中最后我們提到了TomcatServletWebServerFactory類(lèi)實(shí)現(xiàn)了 // ErrorPageRegistry接口。 if (bean instanceof ErrorPageRegistry) { // 處理 postProcessBeforeInitialization((ErrorPageRegistry) bean); } return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } // registry = TomcatServletWebServerFactory private void postProcessBeforeInitialization(ErrorPageRegistry registry) { // 獲取容器中所有ErrorPageRegistrar類(lèi)型的Bean對(duì)象 for (ErrorPageRegistrar registrar : getRegistrars()) { // 將錯(cuò)誤頁(yè)注冊(cè)到TomcatServletWebServerFactory中 registrar.registerErrorPages(registry); } } private Collection<ErrorPageRegistrar> getRegistrars() { if (this.registrars == null) { // 從容器中獲取所有ErrorPageRegistrar類(lèi)型的Bean // 容器默認(rèn)是注冊(cè)了一個(gè)ErrorPageRegistrar this.registrars = new ArrayList<>( this.beanFactory.getBeansOfType(ErrorPageRegistrar.class, false, false).values()); this.registrars.sort(AnnotationAwareOrderComparator.INSTANCE); this.registrars = Collections.unmodifiableList(this.registrars); } return this.registrars; }}
以上的過(guò)程就實(shí)現(xiàn)了自定義錯(cuò)誤頁(yè)的注冊(cè)實(shí)現(xiàn)。
跨域支持
跨源資源共享(CORS)是大多數(shù)瀏覽器實(shí)現(xiàn)的W3C規(guī)范,允許您以靈活的方式指定授權(quán)何種跨域請(qǐng)求。而不是使用一些不太安全、功能不太強(qiáng)大的方法,如IFRAME或JSONP。
從版本4.2開(kāi)始,Spring MVC支持CORS。在Spring Boot應(yīng)用程序中使用帶有@CrossOrigin注釋的控制器方法CORS配置不需要任何特定配置??梢酝ㄟ^(guò)使用自定義的addCorsMappings(CorsRegistry)方法注冊(cè)WebMVCConfiguer bean來(lái)定義全局CORS配置,如下例所示:
proxyBeanMethods = false)public class MyConfiguration { public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**"); } }; }}(