SpringBoot接口如何对异样进行对立封装,并对立返回呢?以上文的参数校验为例,如何优雅的将参数校验的错误信息对立解决并封装返回呢?@pdai
  • SpringBoot接口 - 如何优雅的写Controller并对立异样解决?

    • 为什么要优雅的解决异样
    • 实现案例

      • @ControllerAdvice异样对立解决
      • Controller接口
      • 运行测试
    • 进一步了解

      • @ControllerAdvice还能够怎么用?
      • @ControllerAdvice是如何起作用的(原理)?
    • 示例源码
    • 更多内容

为什么要优雅的解决异样

如果咱们不对立的解决异样,常常会在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");    }}

那怎么实现对立的异样解决,特地是联合参数校验等封装?

实现案例

简略展现通过@ControllerAdvice进行对立异样解决。

@ControllerAdvice异样对立解决

对于400参数谬误异样

/** * Global exception handler. * * @author pdai */@Slf4j@RestControllerAdvicepublic 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。@pdai

@ControllerAdvice还能够怎么用?

除了通过@ExceptionHandler注解用于全局异样的解决之外,@ControllerAdvice还有两个用法:

  • @InitBinder注解

用于申请中注册自定义参数的解析,从而达到自定义申请参数格局的目标;

比方,在@ControllerAdvice注解的类中增加如下办法,来对立解决日期格局的格式化

@InitBinderpublic void handleInitBinder(WebDataBinder dataBinder){    dataBinder.registerCustomEditor(Date.class,            new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));}

Controller中传入参数(string类型)主动转化为Date类型

@GetMapping("testDate")public Date processApi(Date date) {    return date;}
  • @ModelAttribute注解

用来预设全局参数,比方最典型的应用Spring Security时将增加以后登录的用户信息(UserDetails)作为参数。

@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是如何起作用的(原理)?

咱们在Spring根底 - SpringMVC案例和机制的根底上来看@ControllerAdvice的源码实现。

DispatcherServlet中onRefresh办法是初始化ApplicationContext后的回调办法,它会调用initStrategies办法,次要更新一些servlet须要应用的对象,包含国际化解决,requestMapping,视图解析等等。

/**    * This implementation calls {@link #initStrategies}.    */@Overrideprotected 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提供的三种注解性能,从设计和实现的角度必定是实现的代码须要放在initStrategies办法中。

  • @ModelAttribute和@InitBinder解决

具体来看,如果你是设计者,很显然容易想到:对于@ModelAttribute提供的参数预置和@InitBinder注解提供的预处理办法应该是放在一个办法中的,因为它们都是在进入requestMapping办法前做的操作。

如下办法是获取所有的HandlerAdapter,无非就是从BeanFactory中获取(BeanFactory相干常识请参考 Spring进阶- Spring IOC实现原理详解之IOC体系结构设计)

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

@Overridepublic 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

@Overridepublic 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);        }    }}

示例源码

https://github.com/realpdai/t...

更多内容

辞别碎片化学习,无套路一站式体系化学习后端开发: Java 全栈常识体系(https://pdai.tech)