目录
背景
RPC接口
原因
实现
自定义注解
切面
Web接口
原因
实现
自定义注解
切面
建议
参考
最近在读《代码精进之路 从码农到工匠》,书中有讲到异常规范和日志规范,大致是下面这几点:
感觉说得很有道理,就规范进行了实践,对目前项目中的异常和日志进行了V1版本的优化,路漫漫其修远兮,不断的迭代吧。主要进行了两点,一是包装外部RPC接口优化,二是controller层web接口优化。优化点是将方法内进行try-catch处理及参数返回值打印的地方进行了统一处理。
对于被调用的外部接口,最好不要直接调用,要在自己的项目内封装一层,主要做三件事:异常处理、参数及返回记录、特定逻辑处理。刚开始开发的时候不懂,都是直接调用,多个地方都调用后就会发现代码重复度很高,后面有改动的话,就要全局搜索改很多个地方,还是封装一层更为合理,磨刀不误砍柴工。
用于标记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 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;
}
}
因为请求的出口是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 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 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 exceptionHandler(Exception e) {
MyResult result = new MyResult<>();
result.initializeFail(ResultCodes.INNER_ERROR.getCode(), e.getMessage());
log.error("系统异常. result {}", JSON.toJSONString(result), e);
return result;
}
}
Spring aop获取目标对象,方法,接口上的注解
spring aop获取目标对象的方法对象(包括方法上的注解)
Spring AOP 之二:Pointcut注解表达式
AOP Aspect 统一日志、异常处理、数据格式
Spring AOP获取拦截方法的参数名称跟参数值
使用@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler注解实现全局处理Controller层的异常
异常 try – finally 注意的地方