栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

即使有一天,我放弃了自己的身体,也请你,不要放弃我,我亲爱的灵魂.

上一章简单介绍了 SpringBoot自定义Starter(二十四),如果没有看过,请观看上一章

一. AOP 实现日志功能

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

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

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

一.一 pom.xml 添加依赖

        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
        
            top.yueshushu
            starter
            1.0-SNAPSHOT
        
        
            org.projectlombok
            lombok
        
        
        
            org.springframework.boot
            spring-boot-starter-aop
        
        
        
            com.alibaba
            fastjson
            1.2.68
        
    
一.二 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;


@Slf4j
@Aspect //定义切面的注解
@Component
@Order(1)  // 顺序是第一个
public class LogAspect {

    
    private boolean printFullStackTraceForException = true;

    
    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 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

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

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

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

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

接下来,将 切面日志做成 自定义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;


@Data
public class LogVo implements Serializable {
    
    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;


@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;
    
    private Long runTime=DEFAULT_RUNTIME;
    
    private Boolean excFullShow=DEFAULT_EXC_FULL_SHOW;
    
    private Integer resultLength=DEFAULT_RESULT_LENGTH;
    // ...... 其他的默认的信息,后期可以补充其他的
   // ... 构造方法和默认的 setter, gett方法
}
二.四 定义服务 Service

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

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

二.四.一 日志接口 LogService
public interface LogService {
    
   public void logHandler(LogVo logVo);
}
二.四.二 默认的日志接口实现 DefaultLogServiceImpl

打印到控制台

@Log4j2
public class DefaultLogServiceImpl implements LogService{
    
    @Override
    public void logHandler(LogVo logVo) {
        log.info("默认处理日志:>>>"+logVo);
    }
}

二.五 服务配置 LogConfiguration
@Configuration
@EnableConfigurationProperties(MyLogProperties.class)
public class LogConfiguration {

    @Bean
    public MyLogProperties myLogProperties(){
        return new MyLogProperties();
    }
    
    @Bean
    @ConditionalOnMissingBean
    public LogService getLogService(){
        return new DefaultLogServiceImpl();
    }
    
    @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

public class LogAspect{
    private LogService logService;
    private MyLogProperties mylogProperties;

    public LogAspect(MyLogProperties myLogProperties,LogService logService){
        this.logService=logService;
        this.mylogProperties=myLogProperties;
    }
    
    @Pointcut(value ="(@annotation(top.yueshushu.log.MyLog)) ||(execution(public * *..controller.*.*(..))))")
    public void doMyLogCut() {

    }
    
    @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;
    }
    
    @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);
    }
    
    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;
    }
    
    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 + "nt" + strbuff.toString();
        }
        return exceptionName+":"+exceptionMessage;
    }
    
    private String getNowDate(){
        Date now=new Date();
        SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return simpleDateFormat.format(now);
    }

    
    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;
    }
}

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

二.七 测试前准备

将 yjlLog 项目 通过 maven clean install 安装到本地仓库.

二.七.一 StarterApply 项目 pom.xml 添加依赖
  
       
           top.yueshushu
           yjlLog
           1.0-SNAPSHOT
        
二.七.二 准备 controller验证 HelloController
package top.yueshushu.learn.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import top.yueshushu.learn.response.OutputResult;
import top.yueshushu.log.MyLog;


//1. 添加了一个RestController的注解

@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}")
    @MyLog(module = "测试Controller",optType = "测试",description = "可能有异常的情况")
    public OutputResult div(@PathVariable("a") int a, @PathVariable("b") int b){
        return OutputResult.success(a/b);
    }
}

二.八 验证 二.八.一 默认值验证

输入网址: http://127.0.0.1:8081/Log/add/1/0

输入可能有异常的情况: http://127.0.0.1:8081/Log/div/2/1

输入异常信息: http://127.0.0.1:8081/Log/div/2/0

有异常了.

二.八.二 自定义值验证

在 application.yml 配置文件中,进行相应的配置

#配置日志
mylog:
 run-time: 0 #运行时间为0,暂时不设置大值
 result-length: 10  # 结果就返回10
 exc-full-show: false   # 不展示全异常

输入可能有异常的情况: http://127.0.0.1:8081/Log/div/2/1

但是实际返回值,是正常的.

输入异常信息: http://127.0.0.1:8081/Log/div/2/0

处理运行的时间

将 mylog.run-time 时间设置成 100 时,无任何日志打印输出

设置成 1时,有相应的日志打印输出

说明日志 starter 是成功的.

二.九 自定义日志处理方式 LogConfig

现在的日志处理方式,是默认的打印方式. 使用者可以进行自定义,如放置到数据库里面.

在 StarterApply 项目时,进行配置

package top.yueshushu.learn.config;

import org.springframework.context.annotation.Configuration;
import top.yueshushu.log.LogService;
import top.yueshushu.log.LogVo;


@Configuration  //添加注解
public class LogConfig implements LogService {
    //注入数据 Mapper, 进行处理.
    @Override
    public void logHandler(LogVo logVo) {
        //一个简单的输出,让自定义生效
        System.out.println(">>>将其写入到数据库里面,内容是:"+logVo);
    }
}

进行自定义接口验证 ( mylog.run-time 设置值为1)

输入网址: http://127.0.0.1:8081/Log/add/2/2

三. EnableXxx 形式处理 日志Starter

现在我们导入了 日志Starter, 但是我不想使用它,那么该如何处理呢? (现在是只要导入了日志依赖,就处理日志)。

SpringBoot 里面,有大量的 @EnableXxx 的注解形式,

@EnableCaching 添加了,就可以进行缓存 cache,不添加,不可以进行缓存。

我们可以使用到上一章节里面的第三步内容 , 实现动态的热插拔

  1. 动态的进行配置,热插拔效果。达到 我们拥有这个自定义starter,就拥有这些东西,没有自定义starter,就没有这些东西的效果。

这就用到了 @ConditionalOnClass 注解

均在 自定义的 LogStarter 里面进行处理.

三.一 定义标识类的类 LogMarkerConfiguration

LogMarkerConfiguration.java

package top.yueshushu.log;


public class LogMarkerConfiguration {

}

三.二 注解里面引入这个标识类 EnableMyLog
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@documented
@import(LogMarkerConfiguration.class)   //添加了这一个
public @interface EnableMyLog {

}
三.三 Configuration 里面进行配置
@Configuration
@EnableConfigurationProperties(MyLogProperties.class)
@ConditionalOnBean(LogMarkerConfiguration.class)    // 添加这一个
public class LogConfiguration { 
    // 后面均一样
    
}
三.四 测试运行

将 自定义的 starter 重新 maven clean install .

重新运行项目,访问: http://127.0.0.1:8081/Log/add/2/2

发现,并没有日志输出

在启动类上 添加 @EnableMyLog 注解

@SpringBootApplication
@EnableMyLog   //添加注解
public class HelloApplication {
    
}

重新访问,这个时候,控制台便可以打印输出日志信息了

实现了动态的热插拔的效果.

本章节的代码放置在 github 上:

https://github.com/yuejianli/springboot/tree/develop/yjlLog

https://github.com/yuejianli/springboot/tree/develop/StarterApply

谢谢您的观看,如果喜欢,请关注我,再次感谢 !!!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/389780.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号