响应数据结构和异常类型统一后,我们需要统一处理controller的返回数据,全部包装成CommonResponse类型的数据。
import com.zx.eagle.annotation.IgnoreResponseAdvice; import com.zx.eagle.vo.CommonResponse; 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.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import java.util.Objects; /** @author zouwei */ @RestControllerAdvice public class CommonResponseAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports( MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { boolean ignore = false; IgnoreResponseAdvice ignoreResponseAdvice = returnType.getMethodAnnotation(IgnoreResponseAdvice.class); if (Objects.nonNull(ignoreResponseAdvice)) { ignore = ignoreResponseAdvice.value(); return !ignore; } Class<?> clazz = returnType.getDeclaringClass(); ignoreResponseAdvice = clazz.getDeclaredAnnotation(IgnoreResponseAdvice.class); if (Objects.nonNull(ignoreResponseAdvice)) { ignore = ignoreResponseAdvice.value(); } return !ignore; } @Override public Object beforeBodyWrite( Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (Objects.isNull(body)) { return CommonResponse.successInstance(); } if (body instanceof CommonResponse) { return body; } CommonResponse commonResponse = CommonResponse.successInstance(body); return commonResponse; } } 复制代码
很明显,并不是所有的返回对象都需要包装的,比如controller已经返回了CommonResponse,那么就不需要包装
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** @author zouwei */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface IgnoreResponseAdvice { /** * 是否需要被CommonResponseAdvice忽略 * * @return */ boolean value() default true; } 复制代码
其次,我们还需要统一处理异常
import com.google.common.collect.Lists; import com.zx.eagle.common.config.ExceptionTipsStackConfig; import com.zx.eagle.common.exception.EagleException; import com.zx.eagle.common.exception.handler.ExceptionNotifier; import com.zx.eagle.common.vo.CommonResponse; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.validation.Path; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Iterator; import java.util.List; import java.util.Set; /** @author zouwei */ @Slf4j @RestControllerAdvice public class ExceptionResponseAdvice { @Autowired private ExceptionTipsStackConfig exceptionStack; @Autowired(required = false) private List<ExceptionNotifier> exceptionNotifierList; /** * 用户行为导致的错误 * * @param e * @return */ @ExceptionHandler(EagleException.class) public CommonResponse handleEagleException( EagleException e, HttpServletRequest request, HttpServletResponse response) { String massage = handleExceptionMessage(e); CommonResponse commonResponse = CommonResponse.exceptionInstance(e.getCode(), massage, e.getTips()); sendNotify(e, request, response); return commonResponse; } /** * 处理未知错误 * * @param e * @return */ @ExceptionHandler(RuntimeException.class) public CommonResponse handleRuntimeException( RuntimeException e, HttpServletRequest request, HttpServletResponse response) { String error = handleExceptionMessage(e); EagleException unknownException = EagleException.unknownException(error); CommonResponse commonResponse = CommonResponse.exceptionInstance( unknownException.getCode(), error, unknownException.getTips()); sendNotify(unknownException, request, response); return commonResponse; } /** * 处理参数验证异常 * * @param e * @param request * @param response * @return */ @ExceptionHandler(ConstraintViolationException.class) public CommonResponse handleValidException( ConstraintViolationException e, HttpServletRequest request, HttpServletResponse response) { String error = handleExceptionMessage(e); Set<ConstraintViolation<?>> set = e.getConstraintViolations(); Iterator<ConstraintViolation<?>> iterator = set.iterator(); List<EagleException.ValidMessage> list = Lists.newArrayList(); while (iterator.hasNext()) { EagleException.ValidMessage validMessage = new EagleException.ValidMessage(); ConstraintViolation<?> constraintViolation = iterator.next(); String message = constraintViolation.getMessage(); Path path = constraintViolation.getPropertyPath(); Object fieldValue = constraintViolation.getInvalidValue(); String tipsKey = constraintViolation.getMessageTemplate(); validMessage.setTipsKey(tipsKey); validMessage.setFieldName(path.toString()); validMessage.setFieldValue(fieldValue); validMessage.setDefaultMessage(message); list.add(validMessage); } EagleException validException = EagleException.validException(list); sendNotify(validException, request, response); return CommonResponse.exceptionInstance(validException, error); } /** * 发送请求 * * @param exception * @param request * @param response */ private void sendNotify( EagleException exception, HttpServletRequest request, HttpServletResponse response) { if (!CollectionUtils.isEmpty(exceptionNotifierList)) { for (ExceptionNotifier notifier : exceptionNotifierList) { if (notifier.support(exception.getTipsKey())) { notifier.handle(exception, request, response); } } } } /** * 处理异常信息 * * @param e * @return */ private String handleExceptionMessage(Exception e) { String massage = e.getMessage(); String stackInfo = toStackTrace(e); String messageStackInfo = massage + "{" + stackInfo + "}"; // 无论是否让客户端显示堆栈信息,后台都要记录 log.error(messageStackInfo); if (exceptionStack.isShowMessage() && exceptionStack.isShowStack()) { return messageStackInfo; } else if (exceptionStack.isShowMessage()) { return massage; } else if (exceptionStack.isShowStack()) { return stackInfo; } return StringUtils.EMPTY; } /** * 获取异常堆栈信息 * * @param e * @return */ private static String toStackTrace(Exception e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); try { e.printStackTrace(pw); return sw.toString(); } catch (Exception e1) { return StringUtils.EMPTY; } } } 复制代码
为了解决有一些异常需要额外处理的,例如调用第三方接口,接口返回异常并告知费用不够需要充值,这个时候就需要额外通知到相关人员及时充值。为此,特地添加一个接口:
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 异常通知器 * * @author zouwei */ public interface ExceptionNotifier { /** * 是否支持处理该异常 * * @param exceptionKey * @return */ boolean support(String exceptionKey); /** * 处理该异常 * * @param e * @param request */ void handle(EagleException e, HttpServletRequest request, HttpServletResponse response); } 复制代码
为了满足返回的异常信息可配置化,通过配置决定不同的环境返回指定的字段信息
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** @author zouwei */ @Data @Component @ConfigurationProperties(prefix = "exception-tips-stack") public class ExceptionTipsStackConfig { /** 是否显示堆栈信息 */ private boolean showStack = false; /** 是否显示exception message */ private boolean showMessage = false; } 复制代码
application.yaml中配置示例(根据环境配置):
exceptionTipsStack: #异常堆栈是否需要显示 showStack: true #开发提示信息是否需要显示 showMessage: true 复制代码
为了保证返回的数据是指定的json格式,需要配置HttpMessageConverter
import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; /** @author zouwei */ @Configuration public class CustomWebConfigure implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.clear(); converters.add(new MappingJackson2HttpMessageConverter()); } } 复制代码
4.测试
先将application.yaml调整为:
exceptionTipsStack: #异常堆栈是否需要显示 showStack: true #开发提示信息是否需要显示 showMessage: true 复制代码
编写TestController:
import com.zx.eagle.exception.EagleException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import lombok.Data; import org.hibernate.validator.constraints.Length; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import javax.validation.constraints.*; /** @author zouwei */ @Validated @RestController @RequestMapping("/test") public class TestController { /** * * @return * @throws EagleException */ @GetMapping("/user_repeat") public String userRepeat() throws EagleException { throw EagleException.newInstance("USER_REPEAT_REGIST", "用户重复注册了,正常提示"); } /** * 对于用户来说,不应该直接看到NoSuchAlgorithmException,因为这并不是用户造成的,所以应该使用未知错误 * * @return */ @GetMapping("/unknownException") public String unknownException() throws EagleException { final MessageDigest md; try { md = MessageDigest.getInstance("MD4"); } catch (final NoSuchAlgorithmException e) { throw EagleException.unknownException("显然是因为程序没有获取MD5算法导致的异常,这是完全可以避免的"); } return "success"; } @GetMapping("/valid") public String validException( @NotNull(message = "USER_NAME_NOT_NULL") @Length(min = 5, max = 10, message = "USER_NAME_LENGTH_LIMIT") String username, @NotNull(message = "年龄不能为空") @Min(value = 18, message = "年龄必须大于18岁") @Max(value = 70, message = "年龄不能超过70岁") int age) throws EagleException { // return "success"; throw EagleException.newInstance("USER_NO_EXIST", "用户不存在,这个地方要注意"); } @PostMapping("/valid4Post") public String validException2(@Valid @RequestBody User user, BindingResult result) { return "success"; } @Data private static class User { @Length(min = 5, max = 10, message = "USER_NAME_LENGTH_LIMIT") private String username; @Min(value = 18, message = "年龄必须大于18岁") @Max(value = 70, message = "年龄不能超过70岁") private int age; } } 复制代码
测试结果:
url: /test/user_repeat
{ //错误码 "code":"11023", //显示给开发人员看,方便调试 //这个信息里面包括修复信息和异常的堆栈信息,包括异常出在InsuranceController.java:23,也就是这个类的第23行userRepeat方法里面 "error":"用户重复注册了,正常提示{EagleException(code=11023, tips=重复注册)\n\tat com.zx.eagle.exception.EagleException.newInstance(EagleException.java:37)\n\tat com.zx.eagle.controller.InsuranceController.userRepeat(InsuranceController.java:23)\n}", //显示给用户看,明确错误,提示纠正措施 "message":"重复注册", "data":null } 复制代码
url: /test/unknownException
{ //错误码 "code":"-1", //告知出错原因,并给出修复提示,包含堆栈信息,帮助定位异常位置 "error":"显然是因为程序没有获取MD5算法导致的异常,这是完全可以避免的{EagleException(code=-1, tips=未知错误)\n\tat com.zx.eagle.exception.EagleException.newInstance(EagleException.java:37)\n\tat com.zx.eagle.exception.EagleException.unknownException(EagleException.java:47)\n\tat com.zx.eagle.controller.InsuranceController.unknownException(InsuranceController.java:37)}", //显示给用户看,相对于无反应或直接展示异常信息更好 "message":"未知错误", "data":null } 复制代码
url:/test/valid?username=z2341d&age=10
{ code: "-2", error: "test.age: 年龄必须大于18岁{javax.validation.ConstraintViolationException: test.age: 年龄必须大于18岁 at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:117) }", message: "参数验证错误", validMessage: [ { fieldName: "test.age", fieldValue: 10, code: "-2", tips: "年龄必须大于18岁", tipsKey: "年龄必须大于18岁", defaultMessage: "年龄必须大于18岁" } ], data: null } 复制代码
url:/test/valid4Post结果同上
至此,关于异常处理的相关思考和实现阐述完毕。小伙伴们可以依据类似的思考方式实现符合自身实际情况的异常处理方式。
欢迎有过类似思考的小伙伴一起讨论。