SpringBoot自定义日志Starter(二十五)上

一. AOP 实现日志功能

关于 AOP 切面的知识, 可以看:

云深i不知处 前辈的文章: 切面AOP实现权限校验:实例演示与注解全解

我们在 上一章节的 StarterApply 项目中 添加 切面实现日志的功能

一.一 pom.xml 添加依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--添加我们自定义的依赖-->
        <dependency>
            <groupId>top.yueshushu</groupId>
            <artifactId>starter</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--添加aop 的依赖信息-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--添加json-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.68</version>
        </dependency>
    </dependencies>

一.二 HelloController 中添加方法

HelloApplication.java 是普通的启动类.

HelloController 添加三个简单的方法

@RestController
public class HelloController {
    // 无参
    @GetMapping("/")
    public OutputResult toHello(){
        return OutputResult.success("无参数响应");
    }
    //相加
    @GetMapping("/add/{a}/{b}")
    public OutputResult add(@PathVariable("a") int a, @PathVariable("b") int b){
        System.out.println("进行添加");
        return OutputResult.success(a+b);
    }
    //可能会出现异常的方法
    @GetMapping("/div/{a}/{b}")
    public OutputResult div(@PathVariable("a") int a, @PathVariable("b") int b){
        return OutputResult.success(a/b);
    }
}

方法可以正常的访问.

一.三 日志切面 LogAspect

package top.yueshushu.learn.aop;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.InputStreamSource;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

/**
 * 可以使用
 * @date 2021/10/25 10:53
 * @author zk_yjl
 */
@Slf4j
@Aspect //定义切面的注解
@Component
@Order(1)  // 顺序是第一个
public class LogAspect {

    /**
     * 异常,输出完整的stack trace
     */
    private boolean printFullStackTraceForException = true;

    /**
     * (输入输出)参数最大输出长度. -1表示不限制
     */
    private int paramMaxPrintLength = 20000;

    //定义多个切点的位置, 用 || 分隔
    @Pointcut("(execution(public * top.yueshushu.learn.controller.*.*(..))) " +
            "|| (execution(public * top.yueshushu.learn.controller2.*.*(..)))")
    public void log(){
    }

    @Before("log()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
    }

    @AfterReturning(value = "log()", returning = "ret")
    public void doAfterReturning(Object ret) throws Throwable {

    }
    //主要是这一个 
    @Around("log()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {

        String invokeMethodFullPath = buildInvokeMethodFullPath(joinPoint);
        String requestParams = buildRequestParams(joinPoint);
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String method = Optional.ofNullable(attributes).map(attr -> attr.getRequest().getMethod()).orElse(null);
        StringBuilder logInfo = new StringBuilder();
        logInfo.append("request method: ").append(invokeMethodFullPath).append("; ");
        logInfo.append("request type: ").append(method).append("; ");
        logInfo.append("request param: ").append(requestParams).append("; ");

        long startMs =  System.currentTimeMillis();

        try {
            Object result = joinPoint.proceed();

            long cost = System.currentTimeMillis() - startMs;
            logInfo.insert(0, "cost(ms): " + cost + "; ");
            logInfo.append("    -----    response: ").append(toJsonString(result));

            log.info(logInfo.toString());
            return result;
        } catch (Throwable throwable) {

            long cost = System.currentTimeMillis() - startMs;
            logInfo.insert(0, "cost(ms): " + cost + "; ");

            if (printFullStackTraceForException) {
                log.error("error. " + logInfo.toString(), throwable);
            } else {
                log.error("error. " + throwable.getMessage() + "; " + logInfo.toString());
            }
            throw throwable;
        }
    }

    private String toJsonString(Object result) {

        String json = JSON.toJSONString(result);
        if (paramMaxPrintLength <= 0) {
            return json;
        }

        if (json.length() > paramMaxPrintLength) {
            return json.substring(0, paramMaxPrintLength) + "...";
        }

        return json;
    }

    private String buildRequestParams(ProceedingJoinPoint point) {
        try {
            Map<String, Object> requestP = new LinkedHashMap<>();
            Method m = ((MethodSignature) point.getSignature()).getMethod();
            Parameter[] parameters = m.getParameters();
            for (int i = 0, iLen = parameters.length; i < iLen; i++) {
                //过滤Request、Response or InputStreamSource对象,防止序列化异常
                Object arg = point.getArgs()[i];
                if (null == arg) {
                    continue;
                }

                if (arg instanceof HttpServletRequest
                        || arg instanceof HttpServletResponse
                        || arg instanceof InputStreamSource
                        || arg instanceof Errors)  {
                    continue;
                }
                requestP.put(parameters[i].getName(), arg);
            }
            // 提前构造入参信息,防方法内修改入参对象,异常时再构造入参会不准
            return toJsonString(requestP);
        } catch (Exception e) {
            log.warn("请求参数构造失败. error msg: " + e.getMessage());
            return "build error";
        }
    }


    private String buildInvokeMethodFullPath(ProceedingJoinPoint point) {

        Signature signature = point.getSignature();
        Class<?> targetClass = point.getTarget().getClass();
        // 执行方法的路径
        return targetClass.getSimpleName() + " " + signature.getName();
    }

}

一.四 测试

输入网址: http://localhost:8081/Log/add/1/2

SpringBoot自定义日志Starter(二十五)上

可以发现,日志输出打印了

输入网址: http://localhost:8081/Log/div/2/1

SpringBoot自定义日志Starter(二十五)上

输入网址 : http://localhost:8081/Log/div/2/0

SpringBoot自定义日志Starter(二十五)上

可以发现,切面日志是正常工作的.

接下来,将 切面日志做成 自定义Starter 的方式.

二. 自定义 日志Starter

一般都是采用 注解的方式, 哪个方法上添加了相应的注解,就对哪个方法进行日志处理.

二.一 注解 MyLog

MyLog.java

// 适用于方法上
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
    String module() default "默认模块";
    String optType() default "默认类型";
    String description() default "默认说明";
}

二.二 日志展示信息 LogVo

package top.yueshushu.log;

import lombok.Data;

import java.io.Serializable;

/**
 * @ClassName:LogVo
 * @Description 自定义日志的输出展示对象
 * @Author zk_yjl
 * @Date 2021/10/25 10:22
 * @Version 1.0
 * @Since 1.0
 **/
@Data
public class LogVo implements Serializable {
    /**
     @param className 请求的类
     @param methodName 请求的方法名称
     @param params 请求的参数
     @param returnValue 返回值
     @param model 模块  从Log 注解里面拿
     @param optType 操作类型 从Log 注解里面拿
     @param description 操作说明  从 Log 注解里面拿
     @param reqUrl 请求的路径
     @param reqIp 请求的ip地址
     @param reqTime 请求的时间
     @param execTime 执行的时长
     @param excName 异常名称
     @param excInfo 异常的信息
     */
    private String className;
    private String methodName;
    private String params;
    private String returnValue;
    private String model;
    private String optType;
    private String description;
    private String reqUrl;
    private String reqIp;
    private String reqTime;
    private Long execTime;
    private String excName;
    private String excInfo;
    // ... 其他后期扩展字段
    @Override
    public String toString() {
        return "LogVo{" +
                "className='" + className + '\'' +
                ", methodName='" + methodName + '\'' +
                ", params='" + params + '\'' +
                ", returnValue='" + returnValue + '\'' +
                ", model='" + model + '\'' +
                ", optType='" + optType + '\'' +
                ", description='" + description + '\'' +
                ", reqUrl='" + reqUrl + '\'' +
                ", reqIp='" + reqIp + '\'' +
                ", reqTime=" + reqTime +
                ", execTime=" + execTime +
                ", excName='" + excName + '\'' +
                ", excInfo='" + excInfo + '\'' +
                '}';
    }
}

接下来,就跟前面的自定义 Starter 差不多了.

二.三 自定义参数配置 MyLogProperties

MyLogProperties.java

package top.yueshushu.log;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @ClassName:MyLogProperties
 * @Description 日志配置类
 * @Author zk_yjl
 * @Date 2021/10/25 17:07
 * @Version 1.0
 * @Since 1.0
 **/
@ConfigurationProperties("mylog")
public class MyLogProperties {
    /**
     定义默认的信息
     */
    public static final Long DEFAULT_RUNTIME=0L;
    public static Boolean DEFAULT_EXC_FULL_SHOW=true;
    public static Integer DEFAULT_RESULT_LENGTH=0;
    /**
     * @runTime 方法的运行时长  当方法的运行时间> 设置的值时,才记录。 默认为0
     */
    private Long runTime=DEFAULT_RUNTIME;
    /**
     * @excFullShow 异常的信息 是否全部展示 保存
     */
    private Boolean excFullShow=DEFAULT_EXC_FULL_SHOW;
    /**
     * @resultLength 输出结果的长度  0 表示全部输出
     */
    private Integer resultLength=DEFAULT_RESULT_LENGTH;
    // ...... 其他的默认的信息,后期可以补充其他的
   // ... 构造方法和默认的 setter, gett方法
}

二.四 定义服务 Service

日志的处理,可以单独的打印到控制台,可以放置到数据库里面,也可以输出到文件里面。

这个 定义一个接口和 默认的实现

二.四.一 日志接口 LogService

public interface LogService {
    /**
     * 日志处理
     * @date 2021/10/25 19:56
     * @author zk_yjl
     * @param
     * @return void
     */
   public void logHandler(LogVo logVo);
}

二.四.二 默认的日志接口实现 DefaultLogServiceImpl

打印到控制台

@Log4j2
public class DefaultLogServiceImpl implements LogService{
    /**
     * 默认的日志实现,打印到控制台
     * @date 2021/10/29 17:53
     * @author zk_yjl
     * @param logVo
     * @return void
     */
    @Override
    public void logHandler(LogVo logVo) {
        log.info("默认处理日志:>>>"+logVo);
    }
}

二.五 服务配置 LogConfiguration

@Configuration
@EnableConfigurationProperties(MyLogProperties.class)
public class LogConfiguration {

    @Bean
    public MyLogProperties myLogProperties(){
        return new MyLogProperties();
    }
    /**
     外界没有 LogService 的实现时,用默认的
     */
    @Bean
    @ConditionalOnMissingBean
    public LogService getLogService(){
        return new DefaultLogServiceImpl();
    }
    /**
     * 创建切面
     * @date 2021/10/29 17:57
     * @author zk_yjl
     * @param myLogProperties
     * @param logService
     * @return top.yueshushu.log.LogAspect
     */
    @Bean
    public LogAspect logAspect(MyLogProperties myLogProperties,LogService logService){
        return new LogAspect(myLogProperties,logService);
    }
}

二.六 切面配置 LogAspect

package top.yueshushu.log;

import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
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.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

@Aspect
/**
 * 自定义日志输出AOP切面,定义以添加了MyLog注解的所有方法作为连接点,
 * 这些连接点触发时定义对应正常方法返回时通知以及异常发生时通知
 */
public class LogAspect{
    private LogService logService;
    private MyLogProperties mylogProperties;

    public LogAspect(MyLogProperties myLogProperties,LogService logService){
        this.logService=logService;
        this.mylogProperties=myLogProperties;
    }
    /**
     * 切点连接点:在MyLog注解的位置切入  和 controller 下面进行配置
     */
    @Pointcut(value ="(@annotation(top.yueshushu.log.MyLog)) ||(execution(public * *..controller.*.*(..))))")
    public void doMyLogCut() {

    }
    /**
     * MyLog注解方法执行 Around 触发事件
     * @param joinPoint
     * @param
     */
    @Around(value = "doMyLogCut()")
    public Object logInvoke(ProceedingJoinPoint joinPoint) throws Throwable{
        //记录一下时间,
        long beginTime = System.currentTimeMillis();
        Object keys = joinPoint.proceed();
        long time = System.currentTimeMillis() - beginTime;
        LogVo myLogVO = this.getMyLog(joinPoint, keys,null);
        myLogVO.setExecTime(time);
        /**
          运行的时间长 才执行操作
         */
        if(mylogProperties.getRunTime()<=time){
            logService.logHandler(myLogVO);
        }
        return keys;
    }
    /**
     * 异常发生时的通知
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(pointcut = "doMyLogCut()", throwing = "e")
    public void doExceptionMyLog(JoinPoint joinPoint, Throwable e) {
        LogVo myLogVO = this.getMyLog(joinPoint, null,e);
        //出现异常,执行时间为 -1
        myLogVO.setExecTime(-1L);
        // 异常的,一直都进行操作.
        logService.logHandler(myLogVO);
    }
    /**
     * 获取输出日志实体
     * @param joinPoint 触发的连接点
     * @param e 异常对象
     * @return MyLogVO
     */
    private LogVo getMyLog(JoinPoint joinPoint,Object keys,Throwable e){
       // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes
                .resolveReference(RequestAttributes.REFERENCE_REQUEST);
        // 输出日志VO
        LogVo myLogVO = new LogVo();
        try {
            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取切入点所在的方法
            Method method = signature.getMethod();
            // 获取操作
            MyLog opLog = method.getAnnotation(MyLog.class);
            if (opLog != null) {
                myLogVO.setModel(opLog.module());
                myLogVO.setOptType(opLog.optType());
                myLogVO.setDescription(opLog.description());
            }
            // 获取请求的类名
            String className = joinPoint.getTarget().getClass().getName();
            myLogVO.setClassName(className);
            // 获取请求的方法名
            String methodName = method.getName();
            myLogVO.setMethodName(methodName);

            //请求uri
            String uri = request.getRequestURI();
            myLogVO.setReqUrl(uri);
            myLogVO.setReqIp(getIpAddr(request));
            //操作时间点
            myLogVO.setReqTime(getNowDate());

            //异常名称+异常信息
            if(null != e){
                myLogVO.setExcName(e.getClass().getName());
                myLogVO.setExcInfo(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace()));

            }
            //请求的参数,参数所在的数组转换成json
            String params =  Arrays.toString(joinPoint.getArgs());
            myLogVO.setParams(params);
            //返回值
            if(null != keys && Void.class.getName() != keys){
                StringBuilder result =new StringBuilder( JSONObject.toJSONString(keys));
                if(mylogProperties.getResultLength()==0){
                    //表示全部
                    myLogVO.setReturnValue(result.toString());
                }else{
                   String tempResult=result.substring(0,mylogProperties.getResultLength());
                    myLogVO.setReturnValue(tempResult);
                }
            }
            //输出日志
        } catch (Exception ex) {
           // ex.printStackTrace();
        }
        return myLogVO;
    }
    /**
     * 转换异常信息为字符串
     * @param exceptionName
     * @param exceptionMessage
     * @param elements
     * @return
     */
    private String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
        StringBuffer strbuff = new StringBuffer();
        if(mylogProperties.getExcFullShow()){
            for (StackTraceElement stet : elements) {
                strbuff.append(stet + "\n");
            }
            return exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();
        }
        return exceptionName+":"+exceptionMessage;
    }
    /**
     * 获取当前的时间
     * @date 2021/10/26 9:29
     * @author zk_yjl
     * @param
     * @return java.lang.String
     */
    private String getNowDate(){
        Date now=new Date();
        SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return simpleDateFormat.format(now);
    }

    /**
     * 获取访问者的ip地址
     * 注:要外网访问才能获取到外网地址,如果你在局域网甚至本机*问,获得的是内网或者本机的ip
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            //X-Forwarded-For:Squid 服务代理
            String ipAddresses = request.getHeader("X-Forwarded-For");

            if (ipAddresses == null || ipAddresses.length() == 0 ||
                    "unknown".equalsIgnoreCase(ipAddresses)) {
                //Proxy-Client-IP:apache 服务代理
                ipAddresses = request.getHeader("Proxy-Client-IP");
            }

            if (ipAddresses == null || ipAddresses.length() == 0 ||
                    "unknown".equalsIgnoreCase(ipAddresses)) {
                //WL-Proxy-Client-IP:weblogic 服务代理
                ipAddresses = request.getHeader("WL-Proxy-Client-IP");
            }

            if (ipAddresses == null || ipAddresses.length() == 0 ||
                    "unknown".equalsIgnoreCase(ipAddresses)) {
                //HTTP_CLIENT_IP:有些代理服务器
                ipAddresses = request.getHeader("HTTP_CLIENT_IP");
            }

            if (ipAddresses == null || ipAddresses.length() == 0 ||
                    "unknown".equalsIgnoreCase(ipAddresses)) {
                //X-Real-IP:nginx服务代理
                ipAddresses = request.getHeader("X-Real-IP");
            }

            //有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP
            if (ipAddresses != null && ipAddresses.length() != 0) {
                ipAddress = ipAddresses.split(",")[0];
            }

            //还是不能获取到,最后再通过request.getRemoteAddr();获取
            if (ipAddress == null || ipAddress.length() == 0 ||
                    "unknown".equalsIgnoreCase(ipAddresses)) {
                ipAddress = request.getRemoteAddr();
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        return ipAddress;
    }
}

这上面就是一个简单的日志切面配置信息。

上一篇:SpringBoot全局异常处理(三十)上


下一篇:springboot自定义错误页面