SpringController返回值和异常自动包装的问题小结
作者:applebomb
今天遇到一个需求,在不改动原系统代码的情况下。将Controller的返回值和异常包装到一个统一的返回对象中去。
例如原系统的接口
public String myIp(@ApiIgnore HttpServletRequest request);
返回的只是一个IP字符串"0:0:0:0:0:0:0:1",目前接口需要包装为:
{"code":200,"message":"","result":"0:0:0:0:0:0:0:1","success":true}
而原异常跳转到error页面,需要调整为
{ "success": false, "message": "For input string: \"fdsafddfs\"", "code": 500, "result": "message" }
因此就有了2个工作子项需要完成:
1)Exception的处理
2)controller return值的处理
Exception的自动包装
返回的exception处理可以采用@RestControllerAdvice来处理。
建立自己的Advice类,注入国际化资源(异常需要支持多语言)
package org.ccframe.commons.mvc; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; import org.ccframe.commons.filter.CcRequestLoggingFilter; import org.ccframe.commons.util.BusinessException; import org.ccframe.config.GlobalEx; import org.ccframe.subsys.core.dto.Result; import org.springframework.context.MessageSource; import org.springframework.context.NoSuchMessageException; import org.springframework.core.MethodParameter; import org.springframework.http.ResponseEntity; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.NoHandlerFoundException; import javax.servlet.http.HttpServletRequest; import java.util.Locale; @RestControllerAdvice @Log4j2 public class GlobalRestControllerAdvice{ private MessageSource messageSource; //国际化资源 private LocaleResolver localeResolver; private Object[] EMPTY_ARGS = new Object[0]; public GlobalRestControllerAdvice(MessageSource messageSource, LocaleResolver localeResolver){ this.messageSource = messageSource; this.localeResolver = localeResolver; } private Result<String> createError(HttpServletRequest request, Exception e,int code, String msgKey, Object[] args){ Locale currentLocale = localeResolver.resolveLocale(request); String message = ""; try { message = messageSource.getMessage(msgKey, args, currentLocale); }catch (NoSuchMessageException ex){ message = e.getMessage(); }finally { log.error(message); CcRequestLoggingFilter.pendingLog(); //服务器可以记录出错时的请求啦😂 } return Result.error(code, message, msgKey); } @ExceptionHandler(NoHandlerFoundException.class) public Result<?> handlerNoFoundException(HttpServletRequest request, Exception e) { return createError(request, e, HttpStatus.SC_NOT_FOUND, "error.mvc.uriNotFound", EMPTY_ARGS); } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public Result<?> httpRequestMethodNotSupportedException(HttpServletRequest request, HttpRequestMethodNotSupportedException e){ return createError(request,e, HttpStatus.SC_NOT_FOUND,"error.mvc.methodNotSupported", new Object[]{e.getMethod(), StringUtils.join(e.getSupportedMethods(), GlobalEx.DEFAULT_TEXT_SPLIT_CHAR)}); } @ExceptionHandler(BusinessException.class) public Result<?> businessException(HttpServletRequest request, BusinessException e){ return createError(request,e, HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMsgKey(), e.getArgs()); } @ExceptionHandler(ObjectOptimisticLockingFailureException.class) //乐观锁异常 public Result<?> objectOptimisticLockingFailureException(HttpServletRequest request, ObjectOptimisticLockingFailureException e){ return createError(request,e, HttpStatus.SC_INTERNAL_SERVER_ERROR, "errors.db.optimisticLock", EMPTY_ARGS); } @ExceptionHandler(MaxUploadSizeExceededException.class) // 文件上传超限,nginx请设置为10M public Result<?> handleMaxUploadSizeExceededException(HttpServletRequest request, MaxUploadSizeExceededException e) { return createError(request, e, HttpStatus.SC_INTERNAL_SERVER_ERROR, "error.mvc.fileTooLarge", EMPTY_ARGS); } @ExceptionHandler(Exception.class) @ResponseBody public Result<?> handleException(HttpServletRequest request, Exception e) { log.error(e); return createError(request,e, HttpStatus.SC_INTERNAL_SERVER_ERROR, "message", new Object[]{e.getMessage()}); } }
在Config类初始化该Bean(当然也可以使用@Component支持扫描,随你喜欢)
@Bean public GlobalRestControllerAdvice globalRestControllerAdvice(MessageSource messageSource, LocaleResolver localeResolver){ return new GlobalRestControllerAdvice(messageSource, localeResolver); }
controller return值的自动包装
网上的例子有很多坑,包括使用HandlerMethodReturnValueHandler,看了源码才发现。还是ResponseBodyAdvice好使。
建立自己的处理Bean
package org.ccframe.commons.mvc; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.TypeReference; import org.apache.http.HttpStatus; import org.ccframe.commons.util.JsonUtil; import org.ccframe.subsys.core.dto.Result; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import springfox.documentation.swagger.web.ApiResourceController; import java.util.regex.Pattern; @ControllerAdvice public class CcResponseBodyAdvice implements ResponseBodyAdvice<Object> { private static final Pattern CONTROLLER_PATTERN = Pattern.compile("^org\\.ccframe\\.(subsys|sdk)\\.[a-z0-9]+\\.controller\\."); @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return // 只有自己的cotroller类才需要进入,否则swagger都会挂了 CONTROLLER_PATTERN.matcher(returnType.getContainingClass().getName()).find(); } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { System.out.println(returnType.getContainingClass()); Result<Object> result = new Result<>(); result.setResult(body); result.setCode(HttpStatus.SC_OK); if(body instanceof String){ //String返回要特殊处理 return JSON.toJSONString(result); }else { return result; } } }
如果你不需要根据正则来指定包,可以直接用RestControllerAdvice的basePackages属性来过滤
注意这里有2个坑
1)String类型的返回被其它的转换接口StringHttpMessageConverter处理,因此返回要进行JSON编码而不能返回其他类型,否则会报cast类型错,因此就有了String部分的特殊处理方法。
2)controller方法签名返回是void时,不会被处理。为什么,有什么办法?得看spring这段源码:
当returnValue==null时,设置为RequestHandled,也就是提前结束了。后面任何返回的处理都不再进行。所以,如果一定要返回null值的话,可以在controller里返回一个
return new ResponseEntity<Void>(HttpStatus.OK);
这样在返回的值里面就有详细的结构了。
最后要生效的话,在Config类初始它:
@Bean public CcResponseBodyAdvice ccResponseBodyAdvice() { return new CcResponseBodyAdvice(); }
最后。上面两个Bean也可以写在一个,有兴趣的自己尝试。
---------------
null无法被BodyAdvice处理的问题。随着源码跟踪,慢慢知道怎么回事了,我们尝试根本来解决这个问题。从这个图开始:
当返回为null时,mavContainer.isRequestHandled()为true导致了后面的没有处理。
那么想当然的,mavContainer.isRequestHandled()为flase不久解决了吗,向前跟踪,基本到MVC invoke的核心代码里了,发现在invoke前,mavContainer.isRequestHandled()变成了true,再继续跟踪,找到这个方法:
在HandlerMethodArgumentResolverComposite的argumentResolvers看到了上面这个。进行了setRequestHandled。HandlerMethodArgumentResolver是spring controller的参数自动注入机制。看了下源码也没有太多的扩展点,于是只能换个思路。
到此这篇关于SpringController返回值和异常自动包装的文章就介绍到这了,更多相关SpringController返回值内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!