【Java】使用AOP进行异常处理与日志记录

目录

背景

RPC接口

原因

实现

自定义注解

切面

Web接口

原因

实现

自定义注解

切面

建议

参考


背景

最近在读《代码精进之路 从码农到工匠》,书中有讲到异常规范和日志规范,大致是下面这几点:

  • 自定义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获取目标对象,方法,接口上的注解

spring aop获取目标对象的方法对象(包括方法上的注解)

Spring AOP 之二:Pointcut注解表达式

AOP Aspect 统一日志、异常处理、数据格式

Spring AOP获取拦截方法的参数名称跟参数值

使用@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler注解实现全局处理Controller层的异常

异常 try – finally 注意的地方

猜你喜欢

转载自blog.csdn.net/yxz8102/article/details/106303729