目錄
背景
最近在讀《代碼精進之路 從碼農到工匠》,書中有講到異常規範和日誌規範,大致是下面這幾點:
- 自定義BusinessException和SystemException,用於區分業務異常和系統異常,業務異常應該是warn級別,系統異常纔可以記error
- 異常日誌監控:error級別即時報警,warn級別單位時間內數量超過預設閥值也要報警。由於監控報警所以日誌等級要按照規範使用
- 異常要使用AOP統一處理,而不應該使try-catch代碼散落在項目中的各處。使得代碼的可讀性和簡潔性降低
感覺說得很有道理,就規範進行了實踐,對目前項目中的異常和日誌進行了V1版本的優化,路漫漫其修遠兮,不斷的迭代吧。主要進行了兩點,一是包裝外部RPC接口優化,二是controller層web接口優化。優化點是將方法內進行try-catch處理及參數返回值打印的地方進行了統一處理。
RPC接口
原因
對於被調用的外部接口,最好不要直接調用,要在自己的項目內封裝一層,主要做三件事:異常處理、參數及返回記錄、特定邏輯處理。剛開始開發的時候不懂,都是直接調用,多個地方都調用後就會發現代碼重複度很高,後面有改動的話,就要全局搜索改很多個地方,還是封裝一層更爲合理,磨刀不誤砍柴工。
實現
自定義註解
用於標記rpc接口信息
import java.lang.annotation.*;
/**
* 調用外部接口檢查
*
* @yx8102 2020/5/15
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RpcCheck {
// 服務描述
String serviceNameCN() default "外部服務";
// 服務名稱
String serviceNameEN() default "EXTERNAL_SERVICE";
// 方法描述
String methodNameCN() default "外部方法";
// 方法名稱
String methodNameEN() default "EXTERNAL_METHOD";
// 是否打印入參
boolean printParam() default true;
}
切面
進行日誌、異常、耗時統一處理
import com.alibaba.fastjson.JSON;
import com.google.common.base.Stopwatch;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 外部接口調用check
*
* @yx8102 2020/5/15
*/
@Slf4j
@Aspect
@Component
public class RpcCheckAspect {
@SneakyThrows
@Around("@annotation(rpcCheck)")
public Object around(ProceedingJoinPoint point, RpcCheck rpcCheck) {
Object result;
try {
// 拼裝接口入參, 入參名稱-入參值map
Map<String, Object> paramMap = new HashMap<>();
Object[] paramValueArr = point.getArgs();
String[] paramNameArr = ((MethodSignature) point.getSignature()).getParameterNames();
for (int i = 0; i < paramValueArr.length; i++) {
Object paramValue = paramValueArr[i];
if (Objects.isNull(paramValue) || paramValue instanceof HttpServletRequest || paramValue instanceof HttpServletResponse) {
continue;
}
if (paramValue instanceof MultipartFile) {
paramValue = ((MultipartFile) paramValue).getSize();
}
paramMap.put(paramNameArr[i], paramValue);
}
log.info("調用[服務]:{} {} {} {},[參數]:{}", rpcCheck.serviceNameCN(), rpcCheck.serviceNameEN(), rpcCheck.methodNameCN(), rpcCheck.methodNameEN(), rpcCheck.printParam() ? JSON.toJSONString(paramMap) : point.getArgs().length);
Stopwatch stopwatch = Stopwatch.createStarted();
result = point.proceed();
log.info("調用[服務]:{} {} {} {},[返回]:{}", rpcCheck.serviceNameCN(), rpcCheck.serviceNameEN(), rpcCheck.methodNameCN(), rpcCheck.methodNameEN(), JSON.toJSONString(result));
log.info("調用[服務]:{} {} {} {},[耗時]:{}", rpcCheck.serviceNameCN(), rpcCheck.serviceNameEN(), rpcCheck.methodNameCN(), rpcCheck.methodNameEN(), stopwatch.elapsed(TimeUnit.MILLISECONDS));
} catch (NullPointerException e) {
log.error("調用[服務]:{} {} {} {} 異常", rpcCheck.methodNameCN(), rpcCheck.serviceNameEN(), rpcCheck.serviceNameCN(), rpcCheck.methodNameEN(), e);
throw new SystemException(String.format("[服務]: %s,調用發生異常(無可用服務): %s", rpcCheck.methodNameEN(), e.getMessage()));
} catch (Exception e) {
log.error("調用[服務]:{} {} {} {} 異常", rpcCheck.methodNameCN(), rpcCheck.serviceNameEN(), rpcCheck.serviceNameCN(), rpcCheck.methodNameEN(), e);
throw new SystemException(String.format("[服務]: %s,調用發生異常: %s", rpcCheck.methodNameEN(), e.getMessage()));
}
if (Objects.isNull(result)) {
throw new SystemException(String.format("[服務]: %s, 返回爲null", rpcCheck.methodNameEN()));
}
return result;
}
}
Web接口
原因
因爲請求的出口是controller層,所以在這一層增加切面進行統一處理。
實現
自定義註解
用於標記需要個性化處理的接口,比如文件上傳等不需要打印入參的接口
import java.lang.annotation.*;
/**
* controller接口check
*
* @yx8102 2020/5/19
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebCheck {
/**
* 無異常時,是否打印入參, 默認否
* @return
*/
boolean printParam() default false;
/**
* 是否打印返回值, 默認否
* @return
*/
boolean printReturn() default false;
/**
* 是否打印耗時, 默認否
* @return
*/
boolean printTime() default false;
}
切面
進行異常處理、日誌打印
import com.alibaba.fastjson.JSON;
import com.google.common.base.Stopwatch;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* controller 接口日誌、異常統一處理
*
* @xyang010 2020/5/18
*/
@Aspect
@Slf4j
@Component
public class WebCheckAspect {
@Pointcut("execution(public * com.yx.controller..*.*(..)))")
public void controller() {
}
@SneakyThrows
@Around("controller()")
public Object around(ProceedingJoinPoint point) {
// 接口調用計時
Stopwatch stopwatch = Stopwatch.createStarted();
// 接口正常返回
Object result = null;
// 接口異常返回
MyResult<Void> errorResult = new MyResult<>(ResultCodes.UNKNOW_ERROR, ResultCodes.UNKNOW_ERROR.getText());
// controller請求路徑
String targetClassPath = Objects.nonNull(point.getTarget().getClass().getAnnotation(RequestMapping.class)) ? point.getTarget().getClass().getAnnotation(RequestMapping.class).value()[0] : "";
// controller功能描述
String targetClassDesc = Objects.nonNull(point.getTarget().getClass().getAnnotation(Api.class)) ? point.getTarget().getClass().getAnnotation(Api.class).value() : "";
// 接口
Method targetMethod = ((MethodSignature) point.getSignature()).getMethod();
// 接口功能描述
String targetMethodDesc = Objects.nonNull(targetMethod.getAnnotation(ApiOperation.class)) ? targetMethod.getAnnotation(ApiOperation.class).value() : "";
// 接口請求路徑
String methodPost = Objects.nonNull(targetMethod.getAnnotation(PostMapping.class)) ? targetMethod.getAnnotation(PostMapping.class).value()[0] : "";
String methodGet = Objects.nonNull(targetMethod.getAnnotation(GetMapping.class)) ? targetMethod.getAnnotation(GetMapping.class).value()[0] : "";
String methodRequest = Objects.nonNull(targetMethod.getAnnotation(RequestMapping.class)) ? targetMethod.getAnnotation(RequestMapping.class).value()[0] : "";
String methodPath = methodPost + methodGet + methodRequest;
// 接口打印信息配置
WebCheck webCheck = targetMethod.getAnnotation(WebCheck.class);
// 無異常時,是否打印入參
boolean printParam = Objects.nonNull(webCheck) && webCheck.printParam();
// 是否打印返回值
boolean printReturn = Objects.nonNull(webCheck) && webCheck.printReturn();
// 是否打印接口耗時
boolean printTime = Objects.nonNull(webCheck) && webCheck.printTime();
// 拼裝接口入參, 入參名稱-入參值map
Map<String, Object> paramMap = new HashMap<>();
Object[] paramValueArr = point.getArgs();
String[] paramNameArr = ((MethodSignature) point.getSignature()).getParameterNames();
for (int i = 0; i < paramValueArr.length; i++) {
Object paramValue = paramValueArr[i];
if (Objects.isNull(paramValue) || paramValue instanceof HttpServletRequest || paramValue instanceof HttpServletResponse) {
continue;
}
if (paramValue instanceof MultipartFile) {
paramValue = ((MultipartFile) paramValue).getSize();
}
paramMap.put(paramNameArr[i], paramValue);
}
try {
log.info("[接口] {} {} {} {}, " + (printParam ? "[參數] {}" : ">>>>>>>> start"), targetClassDesc, targetMethodDesc, targetClassPath, methodPath, (printParam ? JSON.toJSONString(paramMap) : ""));
result = point.proceed();
} catch (BusinessException e) {
log.warn("[接口][業務異常] {} {} {} {}, [參數] {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, JSON.toJSONString(paramMap), e);
errorResult.initializeFail(ResultCodes.BUSINESS_ERROR.getCode(), targetMethodDesc + ": " + e.getMessage());
} catch (SystemException e) {
log.error("[接口][系統異常] {} {} {} {}, [參數] {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, JSON.toJSONString(paramMap), e);
errorResult.initializeFail(ResultCodes.INNER_ERROR.getCode(), targetMethodDesc + ": " + e.getMessage());
} catch (Exception e) {
log.error("[接口][未知異常] {} {} {} {}, [參數] {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, JSON.toJSONString(paramMap), e);
errorResult.initializeFail(ResultCodes.UNKNOW_ERROR.getCode(), targetMethodDesc + ": " + e.getMessage());
}
if (printReturn) {
log.info("[接口] {} {} {} {}, [返回] {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, JSON.toJSONString(Objects.nonNull(result) ? result : errorResult));
}
if (printTime) {
log.info("[接口] {} {} {} {}, [耗時] {} {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, stopwatch.elapsed(TimeUnit.MILLISECONDS), "ms");
}
if (!printReturn && !printReturn) {
log.info("[接口] {} {} {} {}, >>>>>>>> end", targetClassDesc, targetMethodDesc, targetClassPath, methodPath);
}
return Objects.nonNull(result) ? result : errorResult;
}
}
建議
最好再使用@RestControllerAdvice + @ExceptionHandler 進行controller異常兜底,因爲框架層的異常返回,上面aop是無法攔截的。
示例代碼
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MultipartException;
/**
* 全局異常處理類
*
* @yx8102 2019/10/28
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public MyResult<Void> exceptionHandler(Exception e) {
MyResult<Void> result = new MyResult<>();
result.initializeFail(ResultCodes.INNER_ERROR.getCode(), e.getMessage());
log.error("系統異常. result {}", JSON.toJSONString(result), e);
return result;
}
}
參考
spring aop獲取目標對象的方法對象(包括方法上的註解)
使用@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler註解實現全局處理Controller層的異常