关于spring:SpringBoot接口-如何优雅的写Controller并统一异常处理

48次阅读

共计 11414 个字符,预计需要花费 29 分钟才能阅读完成。

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
@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。@pdai

@ControllerAdvice 还能够怎么用?

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

  • @InitBinder 注解

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

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

@InitBinder
public 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}.
    */
@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 提供的三种注解性能,从设计和实现的角度必定是实现的代码须要放在 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

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

示例源码

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

更多内容

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

正文完
 0