SpringBoot接口如何統(tǒng)一異常處理
為什么要優(yōu)雅的處理異常
如果我們不統(tǒng)一的處理異常,經(jīng)常會在controller層有大量的異常處理的代碼, 比如:
@Slf4j @Api(value = "User Interfaces", tags = "User Interfaces") @RestController @RequestMapping("/user") public class UserController { /** * http://localhost:8080/user/add . * * @param userParam user param * @return user */ @ApiOperation("Add User") @ApiImplicitParam(name = "userParam", type = "body", dataTypeClass = UserParam.class, required = true) @PostMapping("add") public ResponseEntity<String> add(@Valid @RequestBody UserParam userParam) { // 每個接口充斥著大量的異常處理 try { // do something } catch(Exception e) { return ResponseEntity.fail("error"); } return ResponseEntity.ok("success"); } }
那怎么實現(xiàn)統(tǒng)一的異常處理,特別是結合參數(shù)校驗等封裝?
實現(xiàn)案例
簡單展示通過@ControllerAdvice進行統(tǒng)一異常處理。
@ControllerAdvice異常統(tǒng)一處理
對于400參數(shù)錯誤異常
/** * Global exception handler. */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** * exception handler for bad request. * * @param e * exception * @return ResponseResult */ @ResponseBody @ResponseStatus(code = HttpStatus.BAD_REQUEST) @ExceptionHandler(value = { BindException.class, ValidationException.class, MethodArgumentNotValidException.class }) public ResponseResult<ExceptionData> handleParameterVerificationException(@NonNull Exception e) { ExceptionData.ExceptionDataBuilder exceptionDataBuilder = ExceptionData.builder(); log.warn("Exception: {}", e.getMessage()); if (e instanceof BindException) { BindingResult bindingResult = ((MethodArgumentNotValidException) e).getBindingResult(); bindingResult.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage) .forEach(exceptionDataBuilder::error); } else if (e instanceof ConstraintViolationException) { if (e.getMessage() != null) { exceptionDataBuilder.error(e.getMessage()); } } else { exceptionDataBuilder.error("invalid parameter"); } return ResponseResultEntity.fail(exceptionDataBuilder.build(), "invalid parameter"); } }
對于自定義異常
/** * handle business exception. * * @param businessException * business exception * @return ResponseResult */ @ResponseBody @ExceptionHandler(BusinessException.class) public ResponseResult<BusinessException> processBusinessException(BusinessException businessException) { log.error(businessException.getLocalizedMessage(), businessException); // 這里可以屏蔽掉后臺的異常棧信息,直接返回"business error" return ResponseResultEntity.fail(businessException, businessException.getLocalizedMessage()); }
對于其它異常
/** * handle other exception. * * @param exception * exception * @return ResponseResult */ @ResponseBody @ExceptionHandler(Exception.class) public ResponseResult<Exception> processException(Exception exception) { log.error(exception.getLocalizedMessage(), exception); // 這里可以屏蔽掉后臺的異常棧信息,直接返回"server error" return ResponseResultEntity.fail(exception, exception.getLocalizedMessage()); }
Controller接口
(接口中無需處理異常)
@Slf4j @Api(value = "User Interfaces", tags = "User Interfaces") @RestController @RequestMapping("/user") public class UserController { /** * http://localhost:8080/user/add . * * @param userParam user param * @return user */ @ApiOperation("Add User") @ApiImplicitParam(name = "userParam", type = "body", dataTypeClass = UserParam.class, required = true) @PostMapping("add") public ResponseEntity<UserParam> add(@Valid @RequestBody UserParam userParam) { return ResponseEntity.ok(userParam); } }
運行測試
這里用postman測試下:
進一步理解
我們再通過一些問題來幫助你更深入理解
@ControllerAdvice還可以怎么用?
除了通過@ExceptionHandler注解用于全局異常的處理之外,@ControllerAdvice還有兩個用法:
- @InitBinder注解
用于請求中注冊自定義參數(shù)的解析,從而達到自定義請求參數(shù)格式的目的;
比如,在@ControllerAdvice注解的類中添加如下方法,來統(tǒng)一處理日期格式的格式化
@InitBinder public void handleInitBinder(WebDataBinder dataBinder){ dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false)); }
Controller中傳入?yún)?shù)(string類型)自動轉化為Date類型
@GetMapping("testDate") public Date processApi(Date date) { return date; }
- @ModelAttribute注解
用來預設全局參數(shù),比如最典型的使用Spring Security時將添加當前登錄的用戶信息(UserDetails)作為參數(shù)。
@ModelAttribute("currentUser") public UserDetails modelAttribute() { return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); }
所有controller類中requestMapping方法都可以直接獲取并使用currentUser
@PostMapping("saveSomething") public ResponseEntity<String> saveSomeObj(@ModelAttribute("currentUser") UserDetails operator) { // 保存操作,并設置當前操作人員的ID(從UserDetails中獲得) return ResponseEntity.success("ok"); }
@ControllerAdvice是如何起作用的(原理)?
DispatcherServlet中onRefresh方法是初始化ApplicationContext后的回調方法,它會調用initStrategies方法,主要更新一些servlet需要使用的對象,包括國際化處理,requestMapping,視圖解析等等。
/** * This implementation calls {@link #initStrategies}. */ @Override protected void onRefresh(ApplicationContext context) { initStrategies(context); } /** * Initialize the strategy objects that this servlet uses. * <p>May be overridden in subclasses in order to initialize further strategy objects. */ protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); // 文件上傳 initLocaleResolver(context); // i18n國際化 initThemeResolver(context); // 主題 initHandlerMappings(context); // requestMapping initHandlerAdapters(context); // adapters initHandlerExceptionResolvers(context); // 異常處理 initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); }
從上述代碼看,如果要提供@ControllerAdvice提供的三種注解功能,從設計和實現(xiàn)的角度肯定是實現(xiàn)的代碼需要放在initStrategies方法中。
- @ModelAttribute和@InitBinder處理
具體來看,如果你是設計者,很顯然容易想到:對于@ModelAttribute提供的參數(shù)預置和@InitBinder注解提供的預處理方法應該是放在一個方法中的,因為它們都是在進入requestMapping方法前做的操作。
如下方法是獲取所有的HandlerAdapter,無非就是從BeanFactory中獲取
private void initHandlerAdapters(ApplicationContext context) { this.handlerAdapters = null; if (this.detectAllHandlerAdapters) { // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts. Map<String, HandlerAdapter> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerAdapters = new ArrayList<>(matchingBeans.values()); // We keep HandlerAdapters in sorted order. AnnotationAwareOrderComparator.sort(this.handlerAdapters); } } else { try { HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class); this.handlerAdapters = Collections.singletonList(ha); } catch (NoSuchBeanDefinitionException ex) { // Ignore, we'll add a default HandlerAdapter later. } } // Ensure we have at least some HandlerAdapters, by registering // default HandlerAdapters if no other adapters are found. if (this.handlerAdapters == null) { this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class); if (logger.isTraceEnabled()) { logger.trace("No HandlerAdapters declared for servlet '" + getServletName() + "': using default strategies from DispatcherServlet.properties"); } } }
我們要處理的是requestMapping的handlerResolver,作為設計者,就很容易出如下的結構
在RequestMappingHandlerAdapter中的afterPropertiesSet去處理advice
@Override public void afterPropertiesSet() { // Do this first, it may add ResponseBody advice beans initControllerAdviceCache(); if (this.argumentResolvers == null) { List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.initBinderArgumentResolvers == null) { List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers(); this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.returnValueHandlers == null) { List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } } private void initControllerAdviceCache() { if (getApplicationContext() == null) { return; } List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); List<Object> requestResponseBodyAdviceBeans = new ArrayList<>(); for (ControllerAdviceBean adviceBean : adviceBeans) { Class<?> beanType = adviceBean.getBeanType(); if (beanType == null) { throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean); } // 緩存所有modelAttribute注解方法 Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS); if (!attrMethods.isEmpty()) { this.modelAttributeAdviceCache.put(adviceBean, attrMethods); } // 緩存所有initBinder注解方法 Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS); if (!binderMethods.isEmpty()) { this.initBinderAdviceCache.put(adviceBean, binderMethods); } if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) { requestResponseBodyAdviceBeans.add(adviceBean); } } if (!requestResponseBodyAdviceBeans.isEmpty()) { this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans); } }
- @ExceptionHandler處理
@ExceptionHandler顯然是在上述initHandlerExceptionResolvers(context)方法中。
同樣的,從BeanFactory中獲取HandlerExceptionResolver
/** * Initialize the HandlerExceptionResolver used by this class. * <p>If no bean is defined with the given name in the BeanFactory for this namespace, * we default to no exception resolver. */ private void initHandlerExceptionResolvers(ApplicationContext context) { this.handlerExceptionResolvers = null; if (this.detectAllHandlerExceptionResolvers) { // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts. Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values()); // We keep HandlerExceptionResolvers in sorted order. AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers); } } else { try { HandlerExceptionResolver her = context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class); this.handlerExceptionResolvers = Collections.singletonList(her); } catch (NoSuchBeanDefinitionException ex) { // Ignore, no HandlerExceptionResolver is fine too. } } // Ensure we have at least some HandlerExceptionResolvers, by registering // default HandlerExceptionResolvers if no other resolvers are found. if (this.handlerExceptionResolvers == null) { this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class); if (logger.isTraceEnabled()) { logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() + "': using default strategies from DispatcherServlet.properties"); } } }
我們很容易找到ExceptionHandlerExceptionResolver
同樣的在afterPropertiesSet去處理advice
@Override public void afterPropertiesSet() { // Do this first, it may add ResponseBodyAdvice beans initExceptionHandlerAdviceCache(); if (this.argumentResolvers == null) { List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.returnValueHandlers == null) { List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } } private void initExceptionHandlerAdviceCache() { if (getApplicationContext() == null) { return; } List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); for (ControllerAdviceBean adviceBean : adviceBeans) { Class<?> beanType = adviceBean.getBeanType(); if (beanType == null) { throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean); } ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType); if (resolver.hasExceptionMappings()) { this.exceptionHandlerAdviceCache.put(adviceBean, resolver); } if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) { this.responseBodyAdvice.add(adviceBean); } } }
到此這篇關于SpringBoot接口如何統(tǒng)一異常處理的文章就介紹到這了,更多相關SpringBoot接口 異常處理內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Intellij IDEA 2019 最新亂碼問題及解決必殺技(必看篇)
大家在使用Intellij IDEA 的時候會經(jīng)常遇到各種亂碼問題,今天小編給大家分享一些關于Intellij IDEA 2019 最新亂碼問題及解決必殺技,感興趣的朋友跟隨小編一起看看吧2020-04-04spring cloud gateway如何獲取請求的真實地址
這篇文章主要介紹了spring cloud gateway如何獲取請求的真實地址問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05關于SpringBoot大文件RestTemplate下載解決方案
這篇文章主要介紹了SpringBoot大文件RestTemplate下載解決方案,最近結合網(wǎng)上案例及自己總結,寫了一個分片下載tuling/fileServer項目,需要的朋友可以參考下2021-10-10Java編程中使用XFire框架調用WebService程序接口
這篇文章主要介紹了Java編程中使用XFire調用WebService程序接口的方法,WebService是一種跨編程語言和跨操作系統(tǒng)平臺的遠程調用技術,需要的朋友可以參考下2015-12-12詳解基于spring多數(shù)據(jù)源動態(tài)調用及其事務處理
本篇文章主要介紹了基于spring多數(shù)據(jù)源動態(tài)調用及其事務處理 ,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06