请求与响应参数日志记录框架

SpringBoot整合oceanbase,实现oracle无缝切换到oceanbase 随着去“IOE”化的潮流,近些年来,许多与国家相关的项目不约而同地往”国产化“方向靠拢,不可避免地,一些项目可能需要进行数据库迁移。Oceanbase作为蚂蚁集团完全自主研发的国产原生分布式数据库,在TPC-C核TPC-H的测试上有过刷新世界纪录的辉煌成就,已连续9年稳定支撑双11,在阿里集团中多个领域如淘宝、花呗、芝麻信用等投入使用,它具有云原生、强一致性、高度兼容mysql和oracle等特性,非常适合一些需要往国产化数据库迁移的项目。httpshttps。... 阅读详情

前言

实际开发中,如果项目权限管控比较严格,自己又上不去服务器查看日志文件,怎么办?而且日志文件查看也比较繁琐。就随便搞一个数据库记录请求参数与响应数据的日志框架。方便自己排查问题排查问题。

设计

  1. 使用AOP切面技术,将controller层的入参与出参,还有错误信息输出到数据库表logger_info中。
  2. 配合日志级别,使得如果不需要,则不开启,或者只输出特定级别的操作。
  3. 启动时,创建日志表。不需要手动创建。
  4. 定时备份日志表,减少单表数据量过大。

根据以上内容工程分成设计如下:

注: 由于整个项目使用的是mybatis-plus框架,所以添加了service和mapper层,可以使用SQL语句替换

正文

1. 日志实体(日志表)

既然是记录,当然是有记录表了,入参,出参,请求,类,方法,IP,执行时间,都是基本记录。所以就有如下的实体设计。

package com.cah.project.module.logger.domain.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.time.LocalDate;

@Data
@TableName(LoggerInfoEntity.TABLE_NAME)
@ApiModel("日志信息")
public class LoggerInfoEntity {

    public static final String TABLE_NAME = "logger_info";

    @ApiModelProperty("主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @ApiModelProperty("访问的url")
    @TableField("url")
    private String url;

    @ApiModelProperty("类名")
    @TableField("class_name")
    private String className;

    @ApiModelProperty("方法名")
    @TableField("method_name")
    private String methodName;

    @ApiModelProperty("请求的ip地址")
    @TableField("req_ip_adr")
    private String reqIpAdr;

    @ApiModelProperty("响应的ip地址(集群提供)")
    @TableField("rsp_ip_adr")
    private String rspIpAdr;

    @ApiModelProperty("成功标志")
    @TableField("success_ind")
    private Boolean successInd;

    @ApiModelProperty("请求报文头")
    @TableField("req_header")
    private String reqHeader;

    @ApiModelProperty("请求报文体")
    @TableField("req_body")
    private String reqBody;

    @ApiModelProperty("响应报文体")
    @TableField("rsp_body")
    private String rspBody;

    @ApiModelProperty("错误信息")
    @TableField("error_msg")
    private String errorMsg;

    @ApiModelProperty("总耗时")
    @TableField("total_time")
    private Long totalTime;

    @ApiModelProperty("创建时间")
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDate createTime;

}

2. 日志打印级别

这里自定义日志的打印级别,分别为:不打印,打印正常,打印错误,全部打印。根据自身需要,自行修改就好了。

package com.cah.project.module.logger.conf;

/**
 * 功能描述: 日志级别枚举 <br/>
 */
public enum LoggerLevelEnum {

    /** 不打印 */
    NONE,
    /** 打印正常 */
    PRINT,
    /** 打印异常 */
    ERROR,
    /** 全部打印 */
    ALL,

}

3. 日志级别配置

将日志级别放到配置文件中,方便修改调整。

package com.cah.project.module.logger.conf;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "logger.project")
public class LoggerConfig {

    /** 定义日志级别 */
    private LoggerLevelEnum level = LoggerLevelEnum.NONE;

}
复制代码

默认级别为:不打印。如果需要调整级别,则在application.yml配置文件中,添加如下配置即可生效。

# 日志打印级别:不打印-NONE;打印正常-PRINT;打印异常-ERROR;全部打印-ALL
logger:
  project:
    level: PRINT

4. 日志保存服务

在保存日志时,使用@Async注解,达到异步效果,不影响主流程。在接口直接使用default关键字,省事。

package com.cah.project.module.logger.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.cah.project.module.logger.domain.entity.LoggerInfoEntity;
import org.springframework.scheduling.annotation.Async;

/**
 * 功能描述: 日志服务接口 <br/>
 */
public interface ILoggerInfoService extends IService<LoggerInfoEntity> {

    /**
     * 功能描述: 异步保存 <br/>
     *
     * @param info 日志信息
     */
    @Async
    default void saveAsync(LoggerInfoEntity info) {
        save(info);
    }

}

ServiceImpl 和 Mapper 略,直接继承基类就好了。

5. 请求工具类

这个东西,网上找个就好了,主要是为了获取HttpServletRequest的请求头和 IP地址的作用。如果不需要记录,都可以删除了。

package com.cah.project.module.logger.util;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * 功能描述: 请求工具类 <br/>
 */
public class HttpRequestUtil {

    private static final String UNKNOWN = "unknown";
    private static final String LOCALHOST_IP = "127.0.0.1";
    // 客户端与服务器同为一台机器,获取的 ip 有时候是 ipv6 格式
    private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
    private static final String SEPARATOR = ",";

    public static HttpServletRequest getHttpServletRequest() {
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if(servletRequestAttributes == null) {
            return null;
        }
        return servletRequestAttributes.getRequest();
    }

    public static Map<String, String> getHeader() {
        HttpServletRequest request = getHttpServletRequest();
        if(request == null) {
            return new HashMap<>();
        }
        Map<String, String> headerMap = new HashMap<>();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            headerMap.put(headerName, request.getHeader(headerName));
        }
        return headerMap;
    }

    public static String getRealIpAddress() {
        HttpServletRequest request = getHttpServletRequest();
        if (request == null) {
            return "";
        }
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
            if (LOCALHOST_IP.equalsIgnoreCase(ip) || LOCALHOST_IPV6.equalsIgnoreCase(ip)) {
                // 根据网卡取本机配置的 IP
                InetAddress iNet = null;
                try {
                    iNet = InetAddress.getLocalHost();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
                if (iNet != null)
                    ip = iNet.getHostAddress();
            }
        }
        // 对于通过多个代理的情况,分割出第一个 IP
        if (ip != null && ip.length() > 15) {
            if (ip.indexOf(SEPARATOR) > 0) {
                ip = ip.substring(0, ip.indexOf(SEPARATOR));
            }
        }
        return LOCALHOST_IPV6.equals(ip) ? LOCALHOST_IP : ip;
    }

}

6. 请求日志切面(核心)

前面弄的那么多,都是为了给这个切面类服务的。

package com.cah.project.module.logger.aspect;

import cn.hutool.core.net.NetUtil;
import cn.hutool.json.JSONUtil;
import com.cah.project.module.logger.conf.LoggerConfig;
import com.cah.project.module.logger.conf.LoggerLevelEnum;
import com.cah.project.module.logger.domain.entity.LoggerInfoEntity;
import com.cah.project.module.logger.service.ILoggerInfoService;
import com.cah.project.module.logger.util.HttpRequestUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Optional;

/**
 * 功能描述: 日志切面 <br/>
 */
@Order(99)
@Aspect
@Component
public class RequestLogAspect {

    @Autowired
    private LoggerConfig loggerConfig;
    @Autowired
    private ILoggerInfoService loggerInfoService;

    @Around("execution(* com.cah.project..*.controller..*.*(..))")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        // 如果没有开启,则直接返回
        if(LoggerLevelEnum.NONE.equals(loggerConfig.getLevel())) {
            return point.proceed();
        }
        long startTime = System.currentTimeMillis();
        LoggerInfoEntity info = new LoggerInfoEntity();
        // 设置url
        info.setUrl(Optional.ofNullable((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).map(ServletRequestAttributes::getRequest).map(HttpServletRequest::getRequestURI).orElse(""));
        // 设置类名
        info.setClassName(point.getTarget().getClass().getName());
        // 设置方法名
        info.setMethodName(point.getSignature().getName());
        // 设置请求IP地址
        info.setReqIpAdr(HttpRequestUtil.getRealIpAddress());
        // 设置响应IP地址
        info.setRspIpAdr(NetUtil.getLocalhostStr());
        // 设置请求头
        info.setReqHeader(JSONUtil.toJsonStr(HttpRequestUtil.getHeader()));
        // 设置请求体
        info.setReqBody(JSONUtil.toJsonStr(point.getArgs()));
        // 设置请求成功
        info.setSuccessInd(Boolean.TRUE);
        // 定义返回值
        Object obj;
        try {
            Object result = point.proceed();
            info.setRspBody(JSONUtil.toJsonStr(result));
            obj = result;
        } catch (Exception e) {
            // 设置请求异常
            info.setSuccessInd(Boolean.FALSE);
            // 设置异常信息
            info.setErrorMsg(e.getLocalizedMessage());
            throw e;
        } finally {
            // 计算处理时间
            info.setTotalTime(System.currentTimeMillis() - startTime);
            // 如果为全部打印或正常打印,并且为正常标志,记录
            if(LoggerLevelEnum.ALL.equals(loggerConfig.getLevel()) || (LoggerLevelEnum.PRINT.equals(loggerConfig.getLevel()) && info.getSuccessInd())) {
                loggerInfoService.saveAsync(info);
            }
            // 如果为全部打印或者异常打印,并且为异常标志,记录
            if(LoggerLevelEnum.ALL.equals(loggerConfig.getLevel()) || (LoggerLevelEnum.ERROR.equals(loggerConfig.getLevel())) && !info.getSuccessInd()) {
                loggerInfoService.saveAsync(info);
            }
        }
        return obj;
    }

}

7. 启动监听

在项目启动后,需要判断是否需要创建日志表,如果已经存在,则跳过,不存在,则创建日志表。

package com.cah.project.module.logger;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils;
import com.cah.project.module.logger.conf.LoggerConfig;
import com.cah.project.module.logger.sql.DdlSqlFactory;
import com.cah.project.module.logger.sql.IDdlSql;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

/**
 * 功能描述: 日志信息启动执行 <br/>
 * 日志备份,创建日志表等操作
 */
@Component
public class LoggerInfoApplicationListener implements CommandLineRunner {

    @Autowired
    private DataSource dataSource;
    @Autowired
    private LoggerConfig loggerConfig;
    private IDdlSql ddl;

    @Override
    public void run(String... args) throws Exception {
        // 判断数据库类型
        Connection conn = dataSource.getConnection();
        try (Statement statement = conn.createStatement()) {
            DbType dbType = JdbcUtils.getDbType(conn.getMetaData().getURL());
            ddl = DdlSqlFactory.valueOf(dbType.name()).getDdl();
            // 查询表有没有存在
            if(!existTable(statement)) {
                createTable(statement);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 功能描述: 建表 <br/>
     */
    private void createTable(Statement statement) throws SQLException {
        statement.execute(ddl.createTable());
    }

    /**
     * 功能描述: 是否存在表 <br/>
     */
    private boolean existTable(Statement statement) throws SQLException {
        ResultSet resultSet = statement.executeQuery(ddl.queryTable(""));
        resultSet.next();
        return resultSet.getInt(1) == 1;
    }

}

介绍一下 SQL 语句的设计思路。因为可能会扩展到不同的数据库(正常也没那么多屁事)使用枚举,实现单利单利工厂模式,如果真的有需要扩展,则只要修改DdlSqlFactory类和添加一个扩展的IDdlSql实现类即可。

7.1 SQL 接口

queryTable和backTable为什么会有入参呢,是因为备份表的命名规则为:原表名+"_"+日期。如果不想集成Mybaties-plus,则可以将insert语句放在这个接口里。

package com.cah.project.module.logger.sql;

/**
 * 功能描述: 数据库语句 <br/>
 */
public interface IDdlSql {

    /** 查询表是否存在 */
    String queryTable(String date);

    /** 建表语句 */
    String createTable();

    /** 备份表 */
    String backTable(String date);

    /** 删除表 */
    String dropTable();

}

7.2 SQL 枚举工厂

package com.cah.project.module.logger.sql;

import com.cah.project.module.logger.sql.impl.MySQLDdlSql;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 功能描述: SQL执行ddl语句工厂 <br/>
 */
@Getter
@AllArgsConstructor
public enum DdlSqlFactory {

    MYSQL(new MySQLDdlSql()),
    ;

    private final IDdlSql ddl;

}

7.3 SQL 接口实现

package com.cah.project.module.logger.sql.impl;

import cn.hutool.core.util.StrUtil;
import com.cah.project.module.logger.domain.entity.LoggerInfoEntity;
import com.cah.project.module.logger.sql.IDdlSql;

public class MySQLDdlSql implements IDdlSql {

    @Override
    public String queryTable(String date) {
        return "select count(1) from information_schema.tables where table_name ='" + LoggerInfoEntity.TABLE_NAME + (StrUtil.isNotBlank(date) ? "_" + date : "") + "';";
    }

    @Override
    public String createTable() {
        return "create table " + LoggerInfoEntity.TABLE_NAME + "(" +
                "  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',\n" +
                "  `url` varchar(500) COMMENT '访问的url',\n" +
                "  `class_name` varchar(500) COMMENT '类名',\n" +
                "  `method_name` varchar(100) COMMENT '方法名',\n" +
                "  `req_ip_adr` varchar(20) COMMENT '请求的ip地址',\n" +
                "  `rsp_ip_adr` varchar(20) COMMENT '响应的ip地址',\n" +
                "  `success_ind` tinyint COMMENT '成功标志',\n" +
                "  `req_header` text COMMENT '请求报文头',\n" +
                "  `req_body` text COMMENT '请求报文体',\n" +
                "  `rsp_body` text COMMENT '响应报文体',\n" +
                "  `error_msg` text COMMENT '错误信息',\n" +
                "  `total_time` Long COMMENT '总耗时',\n" +
                "  `create_time` datetime DEFAULT NULL COMMENT '创建时间',\n" +
                "  PRIMARY KEY (`id`) USING BTREE,\n" +
                "  KEY `idx_name` (`url`) USING BTREE\n" +
                ") ENGINE=InnoDB COMMENT='日志信息';";
    }

    @Override
    public String backTable(String date) {
        return "rename table " + LoggerInfoEntity.TABLE_NAME + " to " + LoggerInfoEntity.TABLE_NAME + "_" + date + ";";
    }

    @Override
    public String dropTable() {
        return "drop table " + LoggerInfoEntity.TABLE_NAME + ";";
    }

}

8. 日志备份

备份日志选择了通过@Scheduled定时器来处理。使用时,需要在启动类上面添加@EnableScheduling来开启。

这里定时是每天的 00:00:01 秒开始备份。为啥是1秒呢?没有为啥。

package com.cah.project.module.logger;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils;
import com.cah.project.module.logger.conf.LoggerConfig;
import com.cah.project.module.logger.conf.LoggerLevelEnum;
import com.cah.project.module.logger.sql.DdlSqlFactory;
import com.cah.project.module.logger.sql.IDdlSql;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

/**
 * 功能描述: 日志备份任务 <br/>
 */
@Component
public class LoggerInfoBackTask {

    @Autowired
    private DataSource dataSource;
    @Autowired
    private LoggerConfig loggerConfig;

    private IDdlSql ddl;

    /**
     * 功能描述: 每天0点1秒开始执行备份表(确定日期) <br/>
     */
    @Scheduled(cron = "1 0 0 * * ?")
    public void backTask() throws Throwable {
        if(LoggerLevelEnum.NONE.equals(loggerConfig.getLevel())) {
            return;
        }
        Connection conn = dataSource.getConnection();
        try (Statement statement = conn.createStatement()) {
            DbType dbType = JdbcUtils.getDbType(conn.getMetaData().getURL());
            ddl = DdlSqlFactory.valueOf(dbType.name()).getDdl();
            String yesterday = DateUtil.format(DateUtil.yesterday(), DatePattern.PURE_DATE_FORMAT);
            // 查询表有没有存在
            if(!existTable(statement, yesterday)) {
                backTable(statement, yesterday);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 功能描述: 每日备份操作 <br/>
     */
    public void backTable(Statement statement, String yesterday) throws SQLException {
        // 先备份
        statement.execute(ddl.backTable(yesterday));
        // 再创建表
        statement.execute(ddl.createTable());
    }

    /**
     * 功能描述: 是否存在表 <br/>
     */
    private boolean existTable(Statement statement, String yesterday) throws SQLException {
        ResultSet resultSet = statement.executeQuery(ddl.queryTable(yesterday));
        resultSet.next();
        return resultSet.getInt(1) == 1;
    }

}

测试

启动项目后,随便访问访问,看看日志表有没有记录成功就好了。

调整日期,看看有没有进行日志备份

代码

project-logger代码地址

总结

自己项目的日子记录,自己查看起来方便,区别于整体项目框架的日志。方便自己在没有权限的时候排查问题,开个小后门。有条件的话,自己写一个前端,然后做一下权限控制。

04-CGB-DB-LOG-V1.02 日志管理设计说明 业务设计说明 本模块主要是实现对用户行为日志(例如谁在什么时间点执行了什么操作,访问了哪些方法,传递的什么参数,执行时长等)进行记录、查询、删除等操作。其表设计语句如下: CREATE TABLE sys_logs ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) DEFAULT NULL COMMENT ‘登陆用户名’, operation varchar(50) DEFAULT NULL COMMENT ‘用户 阅读详情

相关推荐

服务接口出参入参切面日志设计

切面日志打印

Mr.xing的博客 284

curl 请求日志_SpringBoot记录Http请求日志的方法

在使用Spring Boot开发 web api 的时候希望把 request,request header ,response reponse header , uri, method 等等的信息记录到我们的日志中,方便我们排查问题,也能对系统的数据做一些统计。Spring 使用了 DispatcherServlet 来拦截并分发请求,我们只要自己实现一个 DispatcherServlet 并...

weixin_39867509的博客 624

2-6-1-1 QNX编程入门之进程和线程(一)

文章围绕进程和线程展开介绍。先是将进程类比为房子,线程类比为居住者,阐述其基本概念,如进程占用内存,线程可共享内存。接着讲解同步相关内容,介绍了互斥,线程通过互斥锁来控制对特定资源的访问,像使用浴室一样。还提到优先级,线程按优先级和等待时长来决定获取资源顺序。此外,说明了信号量,以厨房为例,区分计数为 1 和大于 1 的信号情况,且指出信号量可当互斥锁用,但互斥锁是特殊用途信号量,二者存在区别,最后提及后续会介绍其他同步机制。

星原飞火的博客 1324

【工程化】gin框架API及代码使用示例

c.Query。

qq_46063517的博客 3525

AI Agent技能评测框架SkillsBench:从黑盒评估到白盒技能拆解

在人工智能领域,模型评估是衡量技术进展的核心环节。传统评测基准如GLUE、MMLU主要关注模型的通用认知能力,但实际工程应用中,AI Agent需要的是在复杂场景中协调多种能力的“职业技能”。SkillsBench创新性地将Agent能力进行原子化拆解,定义了理解规划、工具调用、推理决策、交互安全等技能维度,并通过领域专家构建的评测集确保生态效度。这一框架为Agent开发提供了精准的“度量衡”,使性能优化从经验驱动转向数据驱动,同时为不同模型和框架提供了公平比较的基础,推动了AI Agent生态从“野蛮生长

weixin_30530339的博客 389

MySQL连接异常:Communications link failure 解决方案

困扰一晚上的问题终于解决了。

weixin_44704776的博客 1086

Scrapy: Spider

官方的帮助文档是给萌新看的嘛????

AI路漫漫 423

基于Python的IMF经济数据爬取与分析:从入门到实战

本文详细介绍了如何使用Python爬取国际货币基金组织(IMF)的经济数据,包括最新的爬虫技术、反反爬策略、数据解析与存储方法。文章从IMF数据源分析开始,逐步讲解如何构建一个完整的爬虫系统,并提供了详细的代码实现和优化建议。通过本文,读者将掌握爬取复杂国际组织数据的关键技术,并能将这些技术应用于其他类似的数据获取场景。关键词:Python爬虫、IMF数据、经济数据获取、数据清洗、反爬策略IMF Data (data.imf.org) - 主要数据门户。

2201_76125261的博客 1439

【Node后端】玩玩Express 框架

一句话说:Express 是一个简洁而灵活的 node.js Web应用框架, 提供了一系列强大特性帮助你创建各种 Web 应用和丰富的 HTTP 工具,如果你不会后端,使用 Express可以帮助我们快速地搭建一个完整功能的网站。 其中Express 框架核心特性: 可以设置中间件来响应 HTTP 请求。 定义了路由表用于执行不同的 HTTP 请求动作。 可以通过向模板传递参数来动态渲染 HTML 页面。 环境搭建 安装node.js 安装express:npm install express --

go 986

【系统架构设计师-2026上半年真题】综合知识-参考答案及详解(75题完全回忆版)

【系统架构设计师-2026上半年真题】综合知识-参考答案及详解(75题完全回忆版)

数据知道的博客 3117

【数据结构】期末考试复习(考点+例题)

1.线性表定义及其存储实现:顺序表和链接表,优缺点分析(顺序存储可随 机访问,链式存储可灵活利用空间),相关操作的时间复杂度。 2.链表的插入删除操作 3.两种特殊的线性表及其应用:栈、队列 4.栈、队列上的(插入/删除)操作及其特点 5.循环队列满和空的条件 6.能够定义线性表、栈、队列的数据存储结构,能够实现相关操作的算法并 分析时间复杂度(例如插入、删除及其它应用等) 7.理解线性结构的应用( 如根据要求能灵活定义线性表结构,能根据线性表 的插入、删除操作解决实际问题) 二叉树基本概念与性质(高

Molly 的博客 9375

Echo框架

Echo框架 1.请求参数的获取 (1.)路径参数获取 e.GET("/users/:id", getUser) id := c.Param("id") (2.)查询参数获取 /show?team=x-men&member=wolverine team := c.QueryParam("team") member := c.QueryParam("member") (3.)Fo...

Madison001的博客 347

手把手教你学PCIE--所有 PF(Physical Function)共同管理和分配 VF

但具体如何分配和使用 VF 取决于硬件设计、驱动程序配置以及实际应用场景。

MHD0815的博客 1464

基于Forest实践|如何更优雅的统一处理请求签名

Forest 是一个开源的 Java HTTP 客户端框架,它能够将 HTTP 的所有请求信息(包括 URL、Header 以及 Body 等信息)绑定到您自定义的 Interface 方法上,能够通过调用本地接口方法的方式发送 HTTP 请求。 本文基于Forest实践,来源于实际业务场景需求,诞生了这篇文章。通过这篇文章,你可以学习到如何更优雅的统一处理基于Forest三方接口请求响应报文存储,如果你读完这篇文章,相信你会有所收获! 文章目录1、背景介绍2、实现方案MethodAnnotationL.

秋夜无霜 2074

小论文/大论文必备 | YOLO多模态目标检测、绘制曲线对比图 | 引入多种绘制曲线对比图,包括mAP0.5,mAP0.5:0.95,Loss损失变化的曲线对比

本文通过可视化方法分析YOLO多模态模型的训练过程,对比不同实验的检测精度和损失函数变化。主要内容包括:1)绘制mAP@0.5和mAP@0.5:0.95曲线评估检测精度;2)分析边界框回归损失(box_loss)、分类损失(cls_loss)和分布式损失(dfl_loss)的收敛趋势;3)提供完整的Python代码实现,支持多实验对比,可生成高清曲线图。该方法能直观展示模型训练效果,便于优化策略评估和参数调优。

Ai缝合怪、助力小伙伴高效发论文 263

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

gin框架学习笔记

gin框架的学习笔记和一些案例练习

m0_52973251的博客 1737

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

learn-claude-code 的 Agent Harness 工程哲学:模型是 Agent,代码只是它的“座驾“

本文基于 [agent-philosophy.md](https://link.gitcode.com/i/632fa627f479faefaa585af422f2e041) 展开,这是 learn-claude-code 仓库中 agent-builder 技能附带的核心理论文档。它回答一个根本问题:当我们说"构建 Agent"时,到底在构建什么?读完本文,你将理解"模型即 Agent、代码即

gitblog_01056的博客 354

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

web中filter、interceptor、aspect

web中filter、interceptor、aspect对于filter、interceptor、aspect 有相似也有区别filterInterceptorAspect 对于filter、interceptor、aspect 有相似也有区别 filter 过滤器可以拦截到方法的请求和响应(ServletRequest request, ServletResponse response),并对...

Lei_lee_coder的博客 482

FastAPI:由入门到精通 1

FastAPI是一个高性能的Python Web框架,用于快速构建API接口服务。摘要如下: 基础路由:通过@app.get装饰器定义路由,支持异步处理函数 参数处理: 路径参数:支持类型注解和Path验证(范围、长度等) 查询参数:使用Query进行参数验证 请求体:通过Pydantic模型定义数据结构,支持Field验证 响应类型:支持JSON/HTML/文件等多种响应格式 高级特性: 自定义响应模型(response_model) 异常处理(HTTPException) .

xiaoxie_life_study 273

Gin笔记

一、环境搭建: Gin是一个很受欢迎的Go web框架 下载前在Goland最好配置一下代理 GOPROXY=https://goproxy.cn,direct 在Goland中创建Go项目之后,下载Gin的依赖 go get -u github.com/gin-gonic/gin 安装postman 修改ip为内网ip router.Run("0.0.0.0:8080") 二、响应 响应jso...

寻找到底哪里有蓝天 135

【测试】Python接口自动化测试框架:从Requests到Allure完整指南

本指南系统梳理了Python接口自动化测试的核心技术栈,从Requests库的请求方法、参数传递(params/data/json区别)到HTTP报文结构,再到Pytest测试框架的用例管理、参数化、Fixture依赖与作用域、conftest.py全局配置,层层深入。同时涵盖YAML数据驱动、JSON Schema响应校验、Logging日志模块以及Allure测试报告的完整集成方案。通过大量代码示例与原理图解,帮助测试开发人员快速搭建稳定、可维护的接口自动化测试体系。

2301_81073406的博客 1253

接口自动化原理和Pytest框架介绍

接口自动化测试作为提升软件质量的关键手段,能够高效、精准地检测接口潜在问题,确保其功能的正确性、稳定性和可靠性,从而为软件系统的整体质量保驾护航。深入掌握接口自动化测试的核心流程,做好各项准备工作,并灵活运用各类拓展功能,全面开展接口自动化测试工作,有效提升测试效率与质量。自动化测试是一门实践性很强的技术,通过编写测试脚本,不断练习各个环节,加深对知识的理解和掌握。可以从简单的业务接口开始,逐步增加业务接口复杂度。

m0_48476055的博客 925

从零构件python接口自动化测试框架:Requests + Pytest + YAML + JSON Schema + Logging + Allure

本文介绍了使用Python构建接口自动化测试框架的核心技术栈:Requests+Pytest+YAML+JSONSchema+Logging+Allure。主要内容包括:Requests处理HTTP请求和会话管理;Pytest实现测试驱动、数据驱动和Fixture管理;YAML存储测试数据和配置;JSONSchema验证复杂JSON响应结构;Logging记录执行日志;Allure生成可视化测试报告。通过将这些工具有机结合,可以构建一个高效、稳定、易维护的企业级接口自动化测试框架,实现从请求发送到结果验证的

2301_81121782的博客 301

Express框架

Web框架 Web框架 Express安装 express的使用过程有两种方式:  方式一:通过express提供的脚手架,直接创建一个应用的骨架;  方式二:从零搭建自己的express应用结构; 方式一:安装express-generator 安装脚手架 npm install -g express-generator 创建项目 express express-demo 安装依赖 npm install 启动项目 node bin/www ◼ 方式二:从零搭建自己的express应用结构; npm

JINGMaNong的博客 254

Python Flask构建RESTful API实战(从小白到专家的进阶之路)

掌握Python Flask开发简单的RESTful API,快速构建轻量级Web服务。适用于前后端分离、微服务等场景,结合路由、请求处理与JSON响应实现核心功能。代码简洁、上手快,适合初学者进阶实战。值得收藏,点击了解完整开发流程。

LearnPlex的博客 344
上一篇: 还在为学不会JVM&G1烦恼吗?看阿里P8源码分析笔记,你想要的都有
下一篇: 10 大 Java 语言特性
Java技术那些事儿
博客等级 码龄5年 2306粉丝 2584原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值