SpringBoot接口 - 如何优雅的写Controller并统一异常处理?
SpringBoot接口如何对异常进行统一封装,并统一返回呢?以上文的参数校验为例,如何优雅的将参数校验的错误信息统一处理并封装返回呢?@pdai
为什么要优雅的处理异常
如果我们不统一的处理异常,经常会在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/tech-pdai-spring-demos
更多内容
告别碎片化学习,无套路一站式体系化学习后端开发: Java 全栈知识体系(https://pdai.tech)
SpringBoot接口 - 如何优雅的写Controller并统一异常处理?的更多相关文章
- SpringBoot接口 - 如何优雅的对接口返回内容统一封装?
在以SpringBoot开发Restful接口时,统一返回方便前端进行开发和封装,以及出现时给出响应编码和信息.@pdai SpringBoot接口 - 如何优雅的对接口返回内容统一封装? RESTf ...
- SpringBoot接口 - 如何优雅的对参数进行校验?
在以SpringBoot开发Restful接口时, 对于接口的查询参数后台也是要进行校验的,同时还需要给出校验的返回信息放到上文我们统一封装的结构中.那么如何优雅的进行参数的统一校验呢? @pdai ...
- 没想到吧,Java开发 API接口可以不用写 Controller了
本文案例收录在 https://github.com/chengxy-nds/Springboot-Notebook 大家好,我是小富~ 今天介绍我正在用的一款高效敏捷开发工具magic-api,顺便 ...
- Spring中毒太深,离开Spring我居然连最基本的接口都不会写了
前言 随着 Spring 的崛起以及其功能的完善,现在可能绝大部分项目的开发都是使用 Spring(全家桶) 来进行开发,Spring也确实和其名字一样,是开发者的春天,Spring 解放了程序员的双 ...
- Spring中毒太深,离开了Spring,我居然连最基本的接口都不会写了¯\_(ツ)_/¯
前言 众所周知,Java必学的框架其中就是SSM,Spring已经融入了每个开发人员的生活,成为了不可或缺的一份子. 随着 Spring 的崛起以及其功能的完善,现在可能绝大部分项目的开发都是使用 S ...
- SpringBoot接口 - 如何生成接口文档之非侵入方式(通过注释生成)Smart-Doc?
通过Swagger系列可以快速生成API文档,但是这种API文档生成是需要在接口上添加注解等,这表明这是一种侵入式方式: 那么有没有非侵入式方式呢, 比如通过注释生成文档? 本文主要介绍非侵入式的方式 ...
- SpringBoot接口 - API接口有哪些不安全的因素?如何对接口进行签名?
在以SpringBoot开发后台API接口时,会存在哪些接口不安全的因素呢?通常如何去解决的呢?本文主要介绍API接口有不安全的因素以及常见的保证接口安全的方式,重点实践如何对接口进行签名.@pdai ...
- SpringBoot接口服务处理Whitelabel Error Page(转)
To switch it off you can set server.error.whitelabel.enabled=false http://stackoverflow.com/question ...
- 2、SpringBoot接口Http协议开发实战8节课(1-6)
1.SpringBoot2.xHTTP请求配置讲解 简介:SpringBoot2.xHTTP请求注解讲解和简化注解配置技巧 1.@RestController and @RequestMapping是 ...
随机推荐
- JavaWeb之如何把请求数据转成实体类
JavaWeb之如何把请求数据转成实体类 自己写个工具类加入下面两个静态方法 自定一个注解类DateTimeFormatting 调用方式User user = util.ObjectFromMap( ...
- 面试必问的8个CSS响应式单位,你知道几个?
大家好,我是半夏,一个刚刚开始写文的沙雕程序员.如果喜欢我的文章,可以关注 点赞 加我微信:frontendpicker,一起学习交流前端,成为更优秀的工程师-关注公众号:搞前端的半夏,了解更多前端知 ...
- NLP教程(2) | GloVe及词向量的训练与评估
作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/36 本文地址:http://www.showmeai.tech/article-det ...
- RESTFul是一种风格
只要符合RESTFul风格的,都可以叫做使用了RESTFul架构,一般的网站里传数据,都是用的?a=1&b=2...如果是RESTFul风格的话,就会是/a/1/b/2..类似于这样的方式来传 ...
- 如何利用 React Hooks 管理全局状态
如何利用 React Hooks 管理全局状态 本文写于 2020 年 1 月 6 日 React 社区最火的全局状态管理库必定是 Redux,但是 Redux 本身就是为了大型管理数据而妥协设计的- ...
- ELK 1.3之kibana
1.安装kibana,直接压缩包安装就可以,kibana默认端口5601 2.配置kibana配置文件 [root@kibana config]# vim /opt/kibana/config/kib ...
- 【ASP.NET Core】URL重写
今天老周和大伙伴们聊聊有关 Url Rewrite 的事情,翻译过来就是 URL 重写. 这里不得不提一下,URL重定向与重写的不同. 1.URL重定向是客户端(通常是浏览器)向服务器请求地址A,然后 ...
- 【算法】选择排序(Selection Sort)(二)
选择排序(Selection Sort) 选择排序(Selection-sort)是一种简单直观的排序算法.它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余 ...
- Flask_WTF实现表单
Flask_WTF实现表单可分为六个步骤: ①导入FlaskForm扩展包(from flask_wtf import FlaskForm) ②导入StringField,PasswordField, ...
- 2020级cpp机考模拟题A卷-#题解1
为了各位朋友的身心健康(不是),我们按照题目难度顺序来写题解. 第一次写题解,希望多点包容和鼓励(恬不知耻 1:谁先输出-4 题意: 输入3个整数,按从大到小的顺序输出,每两个数字间加一个空格. 题解 ...