异常 日志
在后端框架开发中能够抽取出来的公共部分有以下部分
- 自定义枚举类
- 自定义异常信息
- 统一返回信息
- 全局异常处理
- 统一日志打印
自定义枚举类
对于一些经常返回的错误信息,可以将其抽取出来封装成公共部分,然后将变化的作为参数传入。例如在业务中经常要校验某个字段是否为空,如果为空的话就要返回错误信息xxx字段不能为空,那么为什么不将xxx作为一个变量参数传递过来呢。于是就想到了用枚举类定义异常信息,然后用String.format()
方法进行转义
使用方法如下public enum ResponseInfoEnum {
SUCCESS(ResponseResult.OK,"处理成功"),
PARAM_LENGTH_ERROR(ResponseResult.ERROR, "参数:%s,长度错误,max length: %s"),
REQ_PARAM_ERROR(ResponseResult.ERROR, "请求报文必填参数%s缺失"),;
private Integer code;
private String message;
ResponseInfoEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
可以看到生成的错误信息是String.format(ResponseInfoEnum.REQ_PARAM_ERROR.getMessage(),"testValue")
请求报文必填参数testValue缺失
自定义异常信息
首先需要知道为什么要用自定义异常信息呢?使用它有什么好处呢?
- 首先开发中肯定是分模块进行开发的,所以首先统一了自定义异常类就统一了对外异常的展示方式。
- 使用自定义异常继承相关的异常来抛出处理后的异常信息可以隐藏底层的异常,这样更安全,异常信息也更加的直观。自定义异常可以抛出自己想要抛出的信息,可以通过抛出的信息区分异常发生的位置,根据异常名就可以知道哪里有异常,根据异常提示信息进行程序修改。
- 有时候遇到某些校验或者问题时,需要直接结束掉当前的请求,这时便可以通过抛出自定义异常来结束,如果项目中使用了SpringMVC比较新的版本的话有控制器增强,可以通过@ControllerAdvice注解写一个控制器增强类来拦截自定义的异常并响应给前端相应的信息。
自定义异常需要继承
RuntimeException
public class CheckException extends RuntimeException{
public CheckException() {
}
public CheckException(String message) {
super(message);
}
public CheckException(ResponseInfoEnum responseInfoEnum,String ...strings) {
super(String.format(responseInfoEnum.getMessage(),strings));
}
}
统一返回信息
接触的最多的项目就是前后端交互的项目了。所以有一个统一的返回信息不仅对前端来说更加便利,对于后面的AOP代理也有很大的好处。
@Data
@NoArgsConstructor
public class ResponseResult<T> {
public static final Integer OK = 0;
public static final Integer ERROR = 100;
private Integer code;
private String message;
private T data;
}
这样前后端进行交互时就会更加便利了,如果要取业务数据那么就从data中取,去过要取是否成功的标志,那么就从code码中取,如果要取后端返回的信息,那么就从message中取。
全局异常处理
在之前的项目中每个Controller
方法中都充斥着try....catch...
的代码,而catch后的代码都是大同小异,都是封装了一下返回的错误信息之类的。那么为什么不将这些代码抽取出来,利用Spring的全局异常处理简化代码。
@Slf4j
@ControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler(value = Exception.class)
@ResponseBody
public ResponseResult<String> defaultErrorHandler(HttpServletRequest request, Exception exception){
log.error(ControllerLog.getLogPrefix()+"Exception: {}"+exception);
return handleErrorInfo(exception.getMessage());
}
@ExceptionHandler(CheckException.class)
@ResponseBody
public ResponseResult<String> checkExceptionHandler(HttpServletRequest request, CheckException exception){
return handleErrorInfo(exception.getMessage());
}
private ResponseResult<String> handleErrorInfo(String message) {
ResponseResult<String> responseEntity = new ResponseResult<>();
responseEntity.setMessage(message);
responseEntity.setCode(ResponseResult.ERROR);
responseEntity.setData(message);
ControllerLog.destoryThreadLocal();
return responseEntity;
}
}
其中全局异常处理中,自定义的异常就没有打印日志,因为对于自定义的异常是已知的异常,并且错误信息也已经很明确的返回了。而对于未知异常例如Exception
就属于未知的异常,就需要打印日志,如果这里有特殊需求,例如发短信、发邮件通知相关人员的话,这里也能够进行全局的配置。
统一日志打印
统一日志打印只是将项目中公共的打印日志抽取出来,利用AOP来进行打印,例如项目中基本上每个Controller方法的入参和出参都会打印,所以就将此部分抽取出来进行统一管理。
@Slf4j
@Aspect
@Component
public class ControllerLog {
private static final ThreadLocal<Long> START_TIME_THREAD_LOCAL =
new NamedThreadLocal<>("ThreadLocal StartTime");
private static final ThreadLocal<String> LOG_PREFIX_THREAD_LOCAL =
new NamedThreadLocal<>("ThreadLocal LogPrefix");
/**
* <li>Before : 在方法执行前进行切面</li>
* <li>execution : 定义切面表达式</li>
* <p>public * com.example.javadevelopmentframework.javadevelopmentframework.controller..*.*(..))
* <li>public :匹配所有目标类的public方法,不写则匹配所有访问权限</li>
* <li>第一个* :方法返回值类型,*代表所有类型 </li>
* <li>第二个* :包路径的通配符</li>
* <li>第三个..* :表示impl这个目录下所有的类,包括子目录的类</li>
* <li>第四个*(..) : *表示所有任意方法名,..表示任意参数</li>
* </p>
* @param
*/
@Pointcut("execution(public * com.example.javadevelopmentframework.javadevelopmentframework.controller..*.*(..))")
public void exectionMethod(){}
@Before("exectionMethod()")
public void doBefore(JoinPoint joinPoint){
START_TIME_THREAD_LOCAL.set(System.currentTimeMillis());
StringBuilder argsDes = new StringBuilder();
//获取类名
String className = joinPoint.getSignature().getDeclaringType().getSimpleName();
//获取方法名
String methodName = joinPoint.getSignature().getName();
//获取传入目标方法的参数
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
argsDes.append("第" + (i + 1) + "个参数为:" + args[i]+"\n");
}
String logPrefix = className+"."+methodName;
LOG_PREFIX_THREAD_LOCAL.set(logPrefix);
log.info(logPrefix+"Begin 入参为:{}",argsDes.toString());
}
@AfterReturning(pointcut="exectionMethod()",returning = "rtn")
public Object doAfter(Object rtn){
long endTime = System.currentTimeMillis();
long begin = START_TIME_THREAD_LOCAL.get();
log.info(LOG_PREFIX_THREAD_LOCAL.get()+"End 出参为:{},耗时:{}",rtn,endTime-begin);
destoryThreadLocal();
return rtn;
}
public static String getLogPrefix(){
return LOG_PREFIX_THREAD_LOCAL.get();
}
public static void destoryThreadLocal(){
START_TIME_THREAD_LOCAL.remove();
LOG_PREFIX_THREAD_LOCAL.remove();
}
}
测试
在Conroller
中写如下测试
@RestController
public class TestFrameworkController {
@RequestMapping("/success/{value}")
public String success(@PathVariable String value){
return "Return "+value;
}
@RequestMapping("/error/{value}")
public String error(@PathVariable String value){
int i = 10/0;
return "Return "+value;
}
}
单元测试中代码如下
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = JavadevelopmentframeworkApplication.class)
@AutoConfigureMockMvc
public class JavadevelopmentframeworkApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
public void success() throws Exception {
mockMvc.perform(get("/success/11"));
mockMvc.perform(get("/error/11"));
}
}
可以看到打印如下
2019-09-03 20:38:22.248 INFO 73902 --- [ main] c.e.j.j.aop.ControllerLog : TestFrameworkController.successBegin 入参为:第1个参数为:11
2019-09-03 20:38:22.257 INFO 73902 --- [ main] c.e.j.j.aop.ControllerLog : TestFrameworkController.successEnd 出参为:Return 11,耗时:10
2019-09-03 20:38:22.286 INFO 73902 --- [ main] c.e.j.j.aop.ControllerLog : TestFrameworkController.errorBegin 入参为:第1个参数为:11
2019-09-03 20:38:22.288 ERROR 73902 --- [ main] c.e.j.j.aop.ControllerExceptionHandler : TestFrameworkController.errorException: {}java.lang.ArithmeticException: / by zero
可以看到每个访问Controller的方法入参、出参、整个方法的执行时间都已经打印出来了。另外在第二个测试的方法中异常信息捕捉到并打印日志了。