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)