SpringBoot最佳实践之 - 项目中统一记录正常和异常日志

1. 前言

        此篇博客是本人在实际项目开发工作中的一些总结和感悟。是在特定需求背景下,针对项目中统一记录日志(包括正常和错误日志)需求的实现方式之一,并不是普适的记录日志的解决方案。所以阅读本篇博客的朋友,可以参考此篇博客中记录日志的方式,可能会对你有些许帮助和启发。

2. 需求描述

        项目的大背景是:前后端分离项目,后端框架使用SpringBoot+MyBatis,前端使用Vue。前端调用后端API接口时,需要在请求头中传递一个唯一身份标识(为了让服务端知道此次是谁在调用我的接口)。

        在一次完整的调用中,可能会有两种情况:
        (a)调用过程正常,后端给前端返回想要的数据(通过统一返回对象返回数据);
        (b)调用过程异常,后端抛出异常,并通过全局异常处理类进行异常处理,然后通过统一返回对象返回异常码和异常消息。

           现在的需求是:在情况(a),即一次请求正常完成,需要在日志中记录本次的请求和响应的相关信息。包括:
        1)请求身份唯一标识(我需要知道是哪个人访问了我);

        2)请求的接口名(我需要知道你请求的是哪个接口);

        3)请求的参数(我需要知道你此次请求这个接口传递了哪些参数);

        4)请求的时间(我需要知道你此次请求的开始时间);

        5)响应时间(我需要知道此次请求服务端给客户端响应的时间);

        6)请求处理时间(我需要知道此次请求耗费了多少时间);

        7)响应的结果信息(我需要知道此次请求的响应结果是什么);

        8)请求的客户端IP地址(我需要知道此次请求的客户端IP地址);

        9)此次请求是否成功的标识码(如果是成功日志,则标识位是0;失败日志,标识位是1)。

        在情况(b),即此次请求出现异常,也需要在日志中记录本次的请求和响应信息。包括:

        1)请求身份唯一标识(我需要知道是哪个人访问了我);

        2)请求的接口名(我需要知道你请求的是哪个接口);

        3)请求的参数(我需要知道你此次请求这个接口传递了哪些参数);

        4)请求的时间(我需要知道你此次请求的开始时间);

        5)响应时间(我需要知道此次请求服务端给客户端响应的时间);

        6)请求处理时间(我需要知道此次请求耗费了多少时间);

        7)响应的异常信息(我需要知道此次请求的响应的异常信息是什么]);

        8)请求的客户端IP地址(我需要知道此次请求的客户端IP地址);

        9)此次请求是否成功的标识码(如果是成功日志,则标识位是0;失败日志,标识位是1)。

        总结需求就一句话:正常和异常请求我都需要记录日志,只不过正常请求后记录的是正常返回的结果信息,而异常请求后记录的是出现异常的原因。

3. 代码实现

       3.1 logback日志配置

         因为要记录日志,在SpringBoot项目中通常是使用SLF4J作为日志门面,Logback作为底层日志框架实现配合进行日志记录。关于logback的配置信息,可以参考我之前写的一篇博客,你也可以直接把这里的配置信息放到SpringBoot项目的resources目录即可。logback-spring.xml文件的一些记录

      3.2 需要的组件

        实现此需求,需要如下一些功能组件:过滤器、拦截器、存放日志对象的ThreadLocal、统一返回结果对象、统一异常处理、统一响应结果处理器。此处先简单介绍下各个组件在需求中的作用是什么,主要还是靠理解代码实现逻辑。

        1)过滤器:因为需要在请求到达Controller之前获取请求体中的请求参数,而 HttpServletReqeust 获取输入流时仅允许读取一次,如果你直接在拦截器里面获取输入流,拿到里面的请求参数,后续处理过程中就会报java.io.IOException: Stream closed。具体可以参考这篇博文:SpringBoot如何在拦截器中获取@RequestBody参数;

        2)拦截器:在请求进来的时候,可以在拦截器里面获取一些请求信息,如此次请求的身份唯一标识、接口名、请求参数、请求时间、客户端ip地址。然后创建日志记录对象并把这些信息设置到日志记录对象中;

        3)存放日志对象的ThreadLocal:为了把第 2)步中拦截器里面的日志对象放到ThreadLocal里面,便于后续使用;

        4)统一返回结果:SpringBoot的最佳实践之一就是定义全局的统一返回对象,便于和前端联调;

        5)统一异常处理:SpringBoot的最佳实践之一就是在业务处理过程中,如果某些条件校验没通过,就直接抛出异常,然后由统一异常处理类进行统一处理,如打印异常堆栈信息,记录异常日志、通过统一返回结果对象给前端封装错误信息并返回;

        6)统一响应结果处理器:在给前端响应数据之前,可以对要响应的结果进行拦截并做一些事情,此处主要是为了记录一次正常请求过程中的日志信息

3.2.1 过滤器 (HttpServletRequestFilter)

package com.shg.component;

import cn.hutool.extra.servlet.ServletUtil;
import org.springframework.stereotype.Component;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

/***
 * HttpServletRequest 过滤器
 * 解决: request.getInputStream()只能读取一次的问题
 * 目标: 流可重复读
 *
 */
@Component
public class HttpServletRequestFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if (servletRequest instanceof HttpServletRequest) {
            requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest);
        }
        //获取请求中的流,将取出来的字符串,再次转换成流,然后把它放入到新 request对象中.在chain.doFiler方法中传递新的request对象
        if (null == requestWrapper) {
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            filterChain.doFilter(requestWrapper, servletResponse);
        }
    }

    @Override
    public void destroy() {

    }

    /***
     * HttpServletRequest 包装器
     * 解决: request.getInputStream()只能读取一次的问题
     * 目标: 流可重复读
     */
    public class RequestWrapper extends HttpServletRequestWrapper {

        /**
         * 请求体
         */
        private String mBody;

        public RequestWrapper(HttpServletRequest request) {
            super(request);
            // 将body数据存储起来
            mBody = getBody(request);
        }

        /**
         * 获取请求体
         *
         * @param request 请求
         * @return 请求体
         */
        private String getBody(HttpServletRequest request) {
            return ServletUtil.getBody(request);
        }

        /**
         * 获取请求体
         *
         * @return 请求体
         */
        public String getBody() {
            return mBody;
        }

        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            // 创建字节数组输入流
            final ByteArrayInputStream bais = new ByteArrayInputStream(mBody.getBytes(StandardCharsets.UTF_8));

            return new ServletInputStream() {
                @Override
                public boolean isFinished() {
                    return false;
                }

                @Override
                public boolean isReady() {
                    return false;
                }

                @Override
                public void setReadListener(ReadListener readListener) {

                }

                @Override
                public int read() throws IOException {
                    return bais.read();
                }
            };
        }
    }
}


3.2.2 拦截器 (RequestGlobalInterceptor)

package com.shg.component;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.shg.common.ResponseCodeEnum;
import com.shg.exception.BizException;
import com.shg.model.pojo.RecordLog;
import com.shg.utils.LogUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.Objects;

@Slf4j
@Component
public class RequestGlobalInterceptor implements HandlerInterceptor {

    private final StringRedisTemplate stringRedisTemplate;

    public RequestGlobalInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if ("OPTIONS".equals(request.getMethod())) {
            return true;
        }
        if (request.getRequestURI().contains("/swagger-ui.html") ||
                request.getRequestURI().contains("/webjars/springfox-swagger-ui") ||
                request.getRequestURI().contains("swagger") ||
                request.getRequestURI().contains("webjars") ||
                request.getRequestURI().contains("images") ||
                request.getRequestURI().contains("api-docs") ||
                request.getRequestURI().contains("configuration/ui") ||
                request.getRequestURI().contains("configuration/security")) {
            return true;
        }
        String appId = null;
        try {
            appId = request.getHeader("appId");
            String appSecret = request.getHeader("appSecret");
            // 校验appId和appSecret是否合法 TODO
            if(Objects.isNull(appId)){
                throw new BizException(ResponseCodeEnum.APP_ID_NOT_PASSED);
            }
        } finally {
            // --- 日志相关 ---
            // 1. 接口名称
            String requestURI = request.getRequestURI();
            String interfaceName;
            if (requestURI.contains("/api/crypto")) {
                int startIndex = requestURI.indexOf("/api/crypto");
                interfaceName = requestURI.substring(startIndex + "/api/crypto/".length());
            } else {
                interfaceName = requestURI;
            }
            // 2. 请求参数
            String requestParameter = "";
            if (request instanceof HttpServletRequestFilter.RequestWrapper) {
                HttpServletRequestFilter.RequestWrapper repeatedlyRequest = ((HttpServletRequestFilter.RequestWrapper) request);
                requestParameter = StrUtil.removeAny(StrUtil.removeAllLineBreaks(repeatedlyRequest.getBody()), " ");
                //log.info("body参数: " + requestParameter);
            }

            if (StrUtil.isEmpty(requestParameter)) {
                requestParameter = JSONUtil.toJsonStr(request.getParameterMap());
                //log.info("查询字符串参数: " + requestParameter);
            }

            // 创建日志对象
            RecordLog recordLog = new RecordLog();
            recordLog.setAppId(appId);
            recordLog.setInterfaceName(interfaceName);
            recordLog.setArguments(requestParameter);
            recordLog.setRequestTime(DateUtil.format(new Date(), DatePattern.NORM_DATETIME_MS_PATTERN));
            recordLog.setServerIp(request.getRemoteAddr());
            // 把日志对象放到ThreadLocal里面,便于后续使用
            LogUtil.set(recordLog);
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LogUtil.remove();
    }
}

3.2.3 存放日志对象的ThreadLocal (LogUtil)

package com.shg.utils;

import com.shg.model.pojo.RecordLog;

public class LogUtil {

    private static final ThreadLocal<RecordLog> appCacheDtoThreadLocal = new ThreadLocal<>();

    public static void set(RecordLog log) {
        appCacheDtoThreadLocal.set(log);
    }

    public static RecordLog get() {
        return appCacheDtoThreadLocal.get();
    }

    public static void remove() {
        appCacheDtoThreadLocal.remove();
    }
}

3.2.4 控制器Controller

package com.shg.controller;

import com.shg.common.ResultMessage;
import com.shg.model.vo.RandomRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @GetMapping("/test1")
    public ResultMessage<String> test1(@RequestBody RandomRequest randomRequest) {
         int i = 1/0;
        // TODO 调用Service层的业务逻辑
        return ResultMessage.success("这是测试接口..." +randomRequest.getLength());
    }
}

3.2.5 统一返回结果对象 (ResultMessage)

package com.shg.common;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResultMessage<T> {

    private Integer code;
    private String message;
    private T data;
    private long timestamp = System.currentTimeMillis();

    public ResultMessage() {
        this(ResponseCodeEnum.SUCCESS.getCode(), ResponseCodeEnum.SUCCESS.getMessage(), null);
    }

    public ResultMessage(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static <T> ResultMessage<T> success() {
        return new ResultMessage<>();
    }

    public static <T> ResultMessage<T> success(T data) {
        ResultMessage<T> resultMessage = new ResultMessage<>();
        resultMessage.setMessage(ResponseCodeEnum.SUCCESS.getMessage());
        resultMessage.setData(data);
        return resultMessage;
    }

    public static <T> ResultMessage<T> success(String message, T data) {
        ResultMessage<T> resultMessage = new ResultMessage<>();
        resultMessage.setMessage(message);
        resultMessage.setData(data);
        return resultMessage;
    }

    public static <T> ResultMessage<T> success(String message, boolean success) {
        ResultMessage<T> resultMessage = new ResultMessage<>();
        resultMessage.setMessage(message);
        return resultMessage;
    }


    public static <T> ResultMessage<T> errorResult(ResponseCodeEnum responseCodeEnum) {
        return new ResultMessage<>(responseCodeEnum.getCode(), responseCodeEnum.getMessage(), null);
    }

    public static <T> ResultMessage<T> errorResult(Integer code, String message) {
        return new ResultMessage<>(code, message, null);
    }

    public static <T> ResultMessage<T> errorResult(Integer code, String message, T data) {
        return new ResultMessage<>(code, message, data);
    }
}

3.2.6 统一异常处理 (GlobalExceptionHandler)

package com.shg.exception;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.shg.common.ResponseCodeEnum;
import com.shg.common.ResultMessage;
import com.shg.constant.CommonConstant;
import com.shg.model.pojo.RecordLog;
import com.shg.utils.LogUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;
import java.util.Objects;

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Throwable.class)
    @ResponseBody
    public ResultMessage error(Throwable e) {
        e.printStackTrace();
        recordLog(e, e.getMessage());
        return ResultMessage.errorResult(ResponseCodeEnum.SYSTEM_EXCEPTION.getCode(), e.getMessage());
    }

    @ExceptionHandler(BizException.class)
    @ResponseBody
    public ResultMessage error(BizException e) {
        recordLog(e, e.getMessage());
        e.printStackTrace();
        return ResultMessage.errorResult(e.getCode(), e.getMessage());
    }

    private void recordLog(Throwable e, String message) {
        RecordLog recordLog = LogUtil.get();
        if (!Objects.isNull(recordLog)) { // 有一种情况是,当请求方法不支持时,经过过滤器进行放行之后,会直接抛出异常,然后就会在这里进行处理了,此时RecordLog还没有对象,所以这里要判断recordLog是否为空
            String responseTime = DateUtil.format(new Date(), DatePattern.NORM_DATETIME_MS_PATTERN);
            String requestTime = recordLog.getRequestTime();
            long costTime = getCostTime(responseTime, requestTime);
            recordLog.setResponseTime(responseTime);
            recordLog.setResponseInterval(costTime);
            recordLog.setMessage(message);
            recordLog.setResult(CommonConstant.ONE);
            String myLogJson = JSON.toJSONString(recordLog);
            log.error("[EX]:" + System.currentTimeMillis() + " " + myLogJson, e);
        } else {
            log.error(e.getMessage(), e);
        }
    }

    private long getCostTime(String responseTime, String requestTime) {
        Date endTime = DateUtil.parse(responseTime);
        Date startTime = DateUtil.parse(requestTime);
        return DateUtil.between(endTime, startTime, DateUnit.MS);
    }
}

package com.shg.common;

public enum ResponseCodeEnum {

    SUCCESS(0, "success"),
    SYSTEM_EXCEPTION(500, "System internal exception"),
    APP_ID_NOT_PASSED(1001, "The appId is not passed");
    private final int code;
    private final String message;

    ResponseCodeEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

}

3.2.7 统一响应结果处理器 (ResponseBodyAdviceAdapter)

package com.shg.component;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.shg.common.ResponseCodeEnum;
import com.shg.common.ResultMessage;
import com.shg.constant.CommonConstant;
import com.shg.model.pojo.RecordLog;
import com.shg.utils.LogUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.Date;
import java.util.Objects;

/**
 * @DESCRIPTION: 在返回结果前进行日志记录
 * @USER: shg
 * @DATE: 2024/10/24 22:55
 */
@Slf4j
@ControllerAdvice
public class ResponseBodyAdviceAdapter implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {
        if (body instanceof ResultMessage) {
            ResultMessage resultMessage = ((ResultMessage<?>) body);
            if (resultMessage.getCode() == ResponseCodeEnum.SUCCESS.getCode()) {
                recordLog(JSONUtil.toJsonStr(resultMessage));
            }
        }
        return body;
    }
    
    private void recordLog(String message) {
        RecordLog recordLog = LogUtil.get();
        String responseTime = DateUtil.format(new Date(), DatePattern.NORM_DATETIME_MS_PATTERN);
        String requestTime = recordLog.getRequestTime();
        long costTime = getCostTime(responseTime, requestTime);
        recordLog.setResponseTime(responseTime);
        recordLog.setResponseInterval(costTime);
        recordLog.setMessage(message);
        recordLog.setResult(CommonConstant.ZERO);
        String myLogJson = JSON.toJSONString(recordLog);
        if (!Objects.isNull(recordLog.getAppId())) {
            log.info("[SUCCESS RESULT:]" + myLogJson);
        }
    }

    private long getCostTime(String responseTime, String requestTime) {
        Date endTime = DateUtil.parse(responseTime);
        Date startTime = DateUtil.parse(requestTime);
        return DateUtil.between(endTime, startTime, DateUnit.MS);
    }

}

4. 效果演示

4.1. 请求正常执行,记录日志

 前端收到的响应结果:

记录的正常执行日志信息如下:

4.2. 请求异常,记录错误日志

4.21. 异常示例一

前端收到的响应结果如下:

记录的异常日志如下:

4.2.2  异常示例二

模拟业务执行过程中出现异常。前端收到的响应结果如下:

 记录的异常日志如下:

5. 其他

        记录项目运行过程中的正常和异常日志,还有很多其他方法。比如使用AOP等。这里只是我个人在项目开发过程中为了实现需要而使用的一种方式。

        具体代码示例请参考码云:​​​​​​springboot-best-practice: 初次提交

        如果此篇文章对你有帮助,感谢点个赞~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/901281.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用JUC包的AtomicXxxFieldUpdater实现更新的原子性

写在前面 本文一起来看下使用JUC包的AtomicXxxxFieldUpdater实现更新的原子性。代码位置如下&#xff1a; 当前有针对int&#xff0c;long&#xff0c;ref三种类型的支持。如果你需要其他类型的支持的话&#xff0c;也可以照葫芦画瓢。 1&#xff1a;例子 1.1&#xff1a;普…

构建中小企业设备管理平台:Spring Boot应用

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

Android 开发 调节声音 SeekBar自定义样式

效果图 xml布局 mipmap/seekbar图片随意一张图都可以&#xff0c;这里我的图就不贴出来了 <SeekBarandroid:id"id/seekBar"android:layout_marginLeft"8dp"android:layout_width"377dp"android:layout_height"8dp"android:layou…

沈阳乐晟睿浩科技有限公司抖音小店领域的强者

在当今数字化浪潮的推动下&#xff0c;电子商务以其便捷性、高效性和广泛的覆盖面&#xff0c;成为了推动经济发展的新引擎。而抖音小店&#xff0c;作为短视频平台上的新兴电商形态&#xff0c;更是凭借其庞大的用户基础、精准的内容推送机制以及独特的购物体验&#xff0c;迅…

方形件组批优化问题

在中国制造 2025 目标背景之下&#xff0c;发展环境保护型、资源节约型的智能制造业已成为制造行业的当务之急。为了应对客户提出的各式各样的产品需求、订单组批难且产品质量 要求高的问题&#xff0c;使用数学模型辅助企业对定制化产品进行组批优化具有重要意义。本文通 过…

2024.7最新子比主题zibll7.9.2开心版源码+授权教程

授权教程&#xff1a; 1.进入宝塔搭建一个站点 绑定 api.zibll.com 域名 并上传 index.php 文件 2.设置伪静态 3.开启SSL证书&#xff0c;找一个能用的域名证书&#xff0c;将密钥(KEY)和证书(PEM格式)复制进去即可 4.在宝塔文件地址栏中输入 /etc 找到 hosts文件并打开&a…

hcia复习篇

计算机网络&#xff1a; 云技术&#xff1a; 云储存---将数据通过计算机网络传输并储存在第三方服务器。&#xff08;百度网盘&#xff09; 云计算---分布式计算。&#xff08;即共享硬件资源&#xff09; 计算机技术&#xff1a; 文字、图片、视频等---抽象文字。 抽象语言…

【Nginx系列】499错误

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Java应用程序的测试覆盖率之设计与实现(一)-- 总体设计

一、背景 作为测试,如何保证开发人员提交上来的代码都被测试覆盖到,是衡量测试质量的一个重要指标。 本系列文章将要说一说,如何搭建一套测试覆盖率的系统。 包括以下内容: jacoco agent采集执行覆盖率数据jacoco climaven集成jacoco:jacoco-maven-pluginant集成jacoco:…

Linux: network: wireshark IO图的一个问题

今天遇到一个问题&#xff0c;发现wireshark画的IO图&#xff0c;前几秒没有数据&#xff0c;但是根据Raw的pcap看&#xff0c;是有包的&#xff0c;这就迷惑了。 经同事提醒&#xff0c;这个IO在设置了多个画图filter的时候&#xff0c;可能导致开始前几秒没有输出。如下图 这…

HexForge:一款用于扩展安全汇编和十六进制视图的IDA插件

关于HexForge HexForge是一款用于扩展安全汇编和十六进制视图的IDA插件&#xff0c;在该工具的帮助下&#xff0c;广大研究人员可以方便地直接从 IDA Pro 界面数据解码、解密或执行安全数据审计任务。 功能介绍 1、从 IDA 的反汇编或十六进制视图复制原始十六进制&#xff1b;…

安康旅游指南:基于SpringBoot的网站开发实践

第一章 绪论 1.1 研究现状 时代的发展&#xff0c;我们迎来了数字化信息时代&#xff0c;它正在渐渐的改变着人们的工作、学习以及娱乐方式。计算机网络&#xff0c;Internet扮演着越来越重要的角色&#xff0c;人们已经离不开网络了&#xff0c;大量的图片、文字、视频冲击着我…

spring整合使用xml方式整合Druid数据源连接池

1.普通的JDBC数据库连接使用 DriverManager 来获取&#xff0c;每次向数据库建立连接的时候都要将 Connection加载到内存中&#xff0c;再验证用户名和密码(得花费0.05s&#xff5e;1s的时间)。需要数据库连接的时候&#xff0c;就向数据库要求 一个&#xff0c;执行完成后再断…

测试造数,excel转insert语句

目录 excel转sql的insert语句一、背景二、直接上代码 excel转sql的insert语句 一、背景 在实际测试工作中&#xff0c;需要频繁地进行测试造数并插入数据库验证&#xff0c;常规的手写sql语句过于浪费时间&#xff0c;为此简单写个脚本&#xff0c;通过excel来造数&#xff0…

用更多的钱买电脑而不是手机

如果&#xff0c;我们对自己的定义是知识工作者&#xff0c;那么在工作、学习相关的电子设备投入上&#xff0c;真的别舍不得花钱。 需要留意的是&#xff0c;手机&#xff0c;对于大部分在电脑前工作的人&#xff0c;不是工作设备。在我看来&#xff0c;每年投入到电脑的钱&…

Go语言Linux环境搭建以编写第一个Go程序

目录 文章目录 目录Go语言入门1、说明2、CentOS7安装Go3、编写第一个程序3.1、编写程序3.2、运行程序3.3、生成二进制文件4、编写第一个web程序4.1、编写代码4.2、运行程序4.3、测试访问4.4、生成二进制配置Vim-go语法高亮1)、下载和设置Vundle.vim(vim安装插件的工具)2)、…

[Linux网络编程]05-TCP状态和端口复用,shutdown函数(主动方建立/关闭连接状态,被动方建立/关闭连接状态,2MSL时长,TCP其他状态)

一.TCP状态图表示 netstat -apn | grep client 查看客户端网络连接状态 netstat -apn | grep port 查看端口的网络连接状态 二.主动方&#xff0c;被动方TCP连接状态 1. 主动发起连接请求端&#xff1a; CLOSE – 发送SYN – SEND_SYN – 接收 ACK、SYN – SEND_SYN – 发送 A…

查看Chrome安装路

谷歌Google浏览器查看安装路径&#xff0c;浏览器Google Chrome浏览器查看安装路径 chrome://version/ 来源&#xff1a;笔记云

vuex使用modules模块化

1、main.js引入 //引入vuex import store from ./store new Vue({el: #app,router,store,components: { App },template: <App/>,data:function(){return{wbWinList: [] // 定义的变量&#xff0c;全局参数}}, })2、index.js import Vue from vue; import Vuex from …

刷题 - 图论

1 | bfs/dfs | 网格染色 200. 岛屿数量 访问到马上就染色&#xff08;将visited标为 true)auto [cur_x, cur_y] que.front(); 结构化绑定&#xff08;C17&#xff09;也可以不使用 visited数组&#xff0c;直接修改原始数组时间复杂度: O(n * m)&#xff0c;最多将 visited 数…