优化单元测试效率:Spring 工程启动耗时统计
相关文章:
- Java Agent 的简单使用
本文相关代码地址:https://gitee.com/dongguabai/blog
单元测试在软件项目的可持续发展中扮演着不可或缺的角色,这一点毫无疑问。不久前,公司大佬在内部分享时也提到过:单元测试是每个研发工程师的基本技能。
然而,在实际的开发过程中,我们发现很多研发人员并不愿意编写单元测试。其中一个普遍的原因是很多单元测试需要启动 Spring 容器(而许多 Mock 工具本身并不好用),导致启动过程非常耗时。
以我们的核心项目为例,一次单元测试的运行时间接近 5 分钟。考虑到一般需求开发涉及多个接口,再加上中间调试的时间,可能测试几个接口就会耗费一整天的时间。这严重影响了研发工作的效率。
[2023-10-23 12:34:07] [xxx_release] [Dongguabai.local] [INFO] [dfe7d0de51f548dd90e31411312acbd1] 705c1661f4044444b10b53009a32e8e1 main org.springframework.boot.StartupInfoLogger.logStarted(StartupInfoLogger.java:57) - Started SrvCustomerMapperProxyTest in 277.059 seconds (JVM running for 279.544) [12:34:07.659]
也有同学吐槽过:
本文就是希望可以解决这一痛点问题。
切入点
现在的问题是我们的项目启动 Spring 容器慢,那么我们就需要知道启动到底慢在哪里,结合 Spring 的生命周期,就需要去统计每个 Bean 实例化的时间,有几种很常见的切入点:
-
基于
BeanPostProcessor
BeanPostProcessor
提供了两个函数可以帮助我们在 Bean 实例化之后,属性赋值前做一些事情,这也是网上很多博客介绍的一种方案,但是这个方案有个细节要注意,一个 Bean 可能被多个BeanPostProcessor
去处理,所以要注意计算耗时的BeanPostProcessor
的postProcessBeforeInitialization
是第一个调用,且postProcessAfterInitialization
是最后一个调用。这里就涉及到多个BeanPostProcessor
处理的顺序问题。在 Spring 容器中,多个
BeanPostProcessor
,它们的执行顺序是:postProcessBeforeInitialization
方法:按照BeanPostProcessor
在 Spring 容器中的注册顺序依次执行。InitializingBean.afterPropertiesSet
方法:这是Bean初始化后调用的方法。- Bean 配置中的
init-method
:如果有配置,则执行该方法。 postProcessAfterInitialization
方法:按照BeanPostProcessor
在 Spring 容器中的注册顺序依次执行。
很明显,与我们期望的计算耗时的 BeanPostProcessor
的 postProcessBeforeInitialization
和 postProcessAfterInitialization
的执行顺序是有冲突的,所以需要特殊处理。当然如果实际场景影响不大,直接基于 BeanPostProcessor
来做问题也不大。
-
基于
InstantiationStrategy
它负责基于默认构造器,实例化 Bean。需要在
instantiate
函数执行前后进行统计,但整体实现相对比较复杂 -
基于
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
统计的时间范围太宽了,属性赋值的情况也会计入其中,不太准确
-
其实以上三点皆有缺陷,在业务系统中,每个 Bean 的耗时点可能各不一样,有的在实例化阶段,有的在初始化阶段(初始化又分多种情况,如
@PostConstruct
、afterPropertiesSet
,init-method
)。所以这几块要单独统计,便于真正知道 Bean 初始化的耗时
耗时统计实现思路
其实耗时统计说白了就是在方法前后记录一下时间,然后求出差值即可。很常见的方案就是基于代理模式去做,但无论是基于 BeanPostProcessor
还是 InstantiationStrategy
其实都需要考虑 Spring 很多基础组件的顺序问题,相对比较麻烦。本文是基于 Java Agent 实现(关于 Java Agent 可以参看:Java Agent 的简单使用),对 Bean 的实例化和初始化两个阶段分别统计耗时。
耗时记录拦截器:
package blog.dongguabai.spring.bean.timestatistics;import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Argument;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.Callable;/*** @author dongguabai* @date 2023-10-22 14:18*/
public class BeanLifeInterceptor {@RuntimeTypepublic static Object intercept(@Origin Method method, @SuperCall Callable<?> callable, @Argument(1) String beaName, @AllArguments Object[] args, @This Object obj) throws Exception {long start = System.currentTimeMillis();try {return callable.call();} finally {long end = System.currentTimeMillis();if (Objects.equals(method.getName(), "instantiate")) {instantiateLog(beaName, end - start);}if (Objects.equals(method.getName(), "postProcessBeforeInitialization")) {beforeInitializationLog(beaName, start);}if (Objects.equals(method.getName(), "postProcessAfterInitialization")) {afterInitializationLog(beaName, end);}}}private synchronized static void instantiateLog(String beaName, long time) {Log log = LogInterceptor.logs.get(beaName);if (log == null) {log = new Log(beaName);}TimeDate timeDate = log.getTimeDate();timeDate.setBeanName(beaName);timeDate.setInstantiateTime(time);LogInterceptor.logs.put(beaName, log);}private synchronized static void beforeInitializationLog(String beaName, long time) {Log log = LogInterceptor.logs.get(beaName);if (log == null) {log = new Log(beaName);}log.setStartInitializationTime(Math.min(log.getStartInitializationTime() == 0L ? Long.MAX_VALUE : log.getStartInitializationTime(), time));LogInterceptor.logs.put(beaName, log);}private synchronized static void afterInitializationLog(String beaName, long time) {Log log = LogInterceptor.logs.get(beaName);if (log == null) {log = new Log(beaName);}log.setEndInitializationTime(Math.max(time, log.getEndInitializationTime()));LogInterceptor.logs.put(beaName, log);}
}
装载耗时记录拦截器:
package blog.dongguabai.spring.bean.timestatistics;import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;import java.lang.instrument.Instrumentation;/*** @author dongguabai* @date 2023-10-22 14:19*/
public class PreMainAgent {public static void premain(String agentArgs, Instrumentation inst) {new AgentBuilder.Default().type(ElementMatchers.hasSuperType(ElementMatchers.named("org.springframework.beans.factory.support.InstantiationStrategy"))).transform((builder, typeDescription, classLoader, javaModule) -> builder.method(ElementMatchers.named("instantiate")).intercept(MethodDelegation.to(BeanLifeInterceptor.class))).type(ElementMatchers.hasSuperType(ElementMatchers.named("org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter"))).transform((builder, typeDescription, classLoader, javaModule) -> builder.method(ElementMatchers.named("postProcessBeforeInitialization").or(ElementMatchers.named("postProcessAfterInitialization"))).intercept(MethodDelegation.to(BeanLifeInterceptor.class))).installOn(inst);new AgentBuilder.Default().type(ElementMatchers.named(agentArgs.split("#")[0])).transform((builder, typeDescription, classLoader, javaModule) -> builder.method(ElementMatchers.named(agentArgs.split("#")[1])).intercept(MethodDelegation.to(LogInterceptor.class))).installOn(inst);}
}
将 Agent 工程进行打包:
➜ spring-bean-timestatistics mvn package
[INFO] Scanning for projects...
...
[WARNING] Replacing pre-existing project main-artifact file: /Users/dongguabai/IdeaProjects/github/spring-bean-timestatistics/target/dongguabai-spring-bean-timestatistics-1.0-SNAPSHOT.jar
with assembly file: /Users/dongguabai/IdeaProjects/github/spring-bean-timestatistics/target/dongguabai-spring-bean-timestatistics-1.0-SNAPSHOT.jar
...
查看耗时
在启动单元测试时,增加 VM options:
-javaagent:/Users/dongguabai/IdeaProjects/github/spring-bean-timestatistics/target/dongguabai-spring-bean-timestatistics-1.0-SNAPSHOT.jar=com.xxx.SrvCustomerMapperProxyTest#testInsert
这里 com.xxx.SrvCustomerMapperProxyTest
是指要代理的类,testInsert
是指要代理的函数。
接下来启动单元测试看看效果(可以看到这次启动居然只用了 200s,也说明每次启动的时间都不稳定):
...
[2023-10-24 18:04:45] [maf_release] [Dongguabai.local] [INFO] [b4f3a146566b46a49214e48ffabfb615] 06df0988e4364d499d90aa4f7b702b2f main org.springframework.boot.StartupInfoLogger.logStarted(StartupInfoLogger.java:57) - Started SrvCustomerMapperProxyTest in 199.152 seconds (JVM running for 204.326) [18:04:45.346]
...
BEANS TIME STATISTICS:
【shardingDatasourceRoutingDataSource-> totalTime=50526, instantiateTime=76, initializeTime=50450】
【basicDataDubboService-> totalTime=5876, instantiateTime=0, initializeTime=5876】
【com.alibaba.boot.dubbo.DubboProviderAutoConfiguration-> totalTime=4641, instantiateTime=0, initializeTime=4641】
【orgBaseManager-> totalTime=4481, instantiateTime=0, initializeTime=4481】
【srvCommonDubboServiceImpl-> totalTime=2845, instantiateTime=17, initializeTime=2828】
【storageProxyService-> totalTime=2708, instantiateTime=0, initializeTime=2708】...
可以看到各个 Bean 实例化和初始化的耗时,接下来要做的就是逐个分析降低它们的耗时,这就比较简单了。
耗时分析
这里分析几个 Top 耗时。
shardingDatasourceRoutingDataSource
可以看到耗时在 initializeTime
,而这个 Bean 实现了 AbstractRoutingDataSource
,重写了 afterPropertiesSet
方法:
private void initDataSource(DruidDataSource druidDataSource) {try {druidDataSource.init();} catch (SQLException e) {throw new RuntimeException(e);}}
而耗时主要就在连接池初始化,这个其实有两种方案:
-
异步初始化
设置一下即可,但是这个要注意,可能我们跑单测的时候涉及到数据库访问,如果此时连接池没有初始化完成的话会出现异常
druidDataSource.setAsyncInit(true);
-
初始化连接数为 0
就是不需要一开始就设置数据库连接:
ds.setInitialSize(NumberUtils.toInt(env.getProperty("ds.initialSize"), 10));
我这边使用第二种方案。
basicDataDubboService
这是一个普通被 @Service
标注的 Bean,可以看到也是耗时在 initializeTime
,这是因为这个项目使用的是自己封装的 Dubbo 接口,依赖注入 Dubbo Reference 是依赖于 BeanPostProcessor
实现。
在 basicDataDubboService
中引入 Dubbo 接口方式是使用的自定义注解 @DubboConsumer
:
@Service
@Slf4j
public class BasicDataDubboService {@DubboConsumer(check = false) private FaultService faultService;
}
而 @DubboConsumer
会被 BeanPostProcessor
处理:
@Beanpublic BeanPostProcessor beanPostProcessor() {return new BeanPostProcessor() {@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName)throws BeansException {Class<?> objClz = bean.getClass();if (org.springframework.aop.support.AopUtils.isAopProxy(bean)) {objClz = org.springframework.aop.support.AopUtils.getTargetClass(bean);}try {for (Field field : objClz.getDeclaredFields()) {DubboConsumer dubboConsumer = field.getAnnotation(DubboConsumer.class);
...
分析后可以设置 @DubboConsumer
属性 lazy
为 true
,相当于 ReferenceBean
的 lazy
属性,这样可以懒加载创建 Reference 代理。
同时也发现有很多无用的注入,这里可以直接清除:
com.alibaba.boot.dubbo.DubboProviderAutoConfiguration
这个也是团队内部封装的 Dubbo 配置类,它依赖 @PostConstruct
对外暴露 Dubbo 服务:
@PostConstructpublic void init() throws Exception {Map<String, Object> beans = this.applicationContext.getBeansWithAnnotation(Service.class);for (Map.Entry<String, Object> entry : beans.entrySet()) {this.initProviderBean(entry.getKey(), entry.getValue());if(this.properties.getProviderDefaultGroup()){this.initProviderBeanWithGroup(entry.getKey(), entry.getValue());}}}
private void initProviderBeanWithGroup(String beanName, Object bean) throws Exception {...serviceConfig.export();}
可以看到整个实现也是相对比较粗糙(比如根本没必要获取全部的 @Service
标注的 Bean,可以自定义一个实现注解,降低查找范围),先不纠结于细节,至少本地启动单元测试是没必要暴露 Dubbo 服务的,所以是否暴露 Dubbo 服务可以基于 profile 做一个限制。
结果查看
上面三耗时分析,除了 com.alibaba.boot.dubbo.DubboProviderAutoConfiguration
由于涉及到代码改造,暂时没有处理,其余的两个已经处理,重新启动,看看效果:
...
[2023-10-24 18:52:03] [maf_release] [Dongguabai.local] [INFO] [d53b3ebcc40240f19b12ca0bdf434436] 375000afa08a48b49f9121a5c4760386 main org.springframework.boot.StartupInfoLogger.logStarted(StartupInfoLogger.java:57) - Started SrvCustomerMapperProxyTest in 142.282 seconds (JVM running for 148.136) [18:52:03.086]
...
【basicDataDubboService-> totalTime=5567, instantiateTime=0, initializeTime=5567】
【shardingDatasourceRoutingDataSource-> totalTime=272, instantiateTime=70, initializeTime=202】
...
可以看到整个启动速度大大提升,主要还是 shardingDatasourceRoutingDataSource
的初始化速度从 50450ms 降低至 202ms。至于 basicDataDubboService
,效果相对一般,但也从中找到了内部封装的 Dubbo 组件优化点。
总结
基于 TOP3 耗时 Bean 的分析来看,耗时有这么几种原因:
- 数据库连接可以延迟初始化
- 移除无用的依赖注入
- 内部封装的 Dubbo 组件存在很多优化点,如:
- Dubbo Provider 在本地没必要对外暴露服务
- 懒加载创建 Reference 代理
尽管不同项目的耗时原因各有差异,但只要能够确定具体的耗时点,问题就不会太棘手。本文介绍了一个简单的工具,用于获取 Spring Bean 初始化的时间,借此能够准确定位具体的 Bean,从而有针对性地进行优化。通过优化这些耗时操作,就能够有效提升整个项目的启动速度,显著增强研发工作的效率。
References
- https://github.com/microsphere-projects/microsphere-spring
- https://github.com/raphw/byte-buddy/issues/748
欢迎关注公众号:
相关文章:

优化单元测试效率:Spring 工程启动耗时统计
相关文章: Java Agent 的简单使用 本文相关代码地址:https://gitee.com/dongguabai/blog 单元测试在软件项目的可持续发展中扮演着不可或缺的角色,这一点毫无疑问。不久前,公司大佬在内部分享时也提到过:单元测试是…...
华纳云:连接mysql出现2059错误怎么解决
MySQL连接错误2059通常表示MySQL服务器拒绝了连接。这种错误可能由多种原因引起,以下是一些可能的解决方法: 检查MySQL服务器是否正在运行: 确保MySQL服务器正在正常运行。您可以使用以下命令检查MySQL服务器的状态: systemctl st…...

零基础Linux_22(多线程)线程控制和和C++的多线程和笔试选择题
目录 1. 线程控制 1.1 线程创建(pthread_create) 1.2 线程结束(pthread_exit) 1.3 线程等待(pthread_join) 1.4 线程取消(pthread_cancel结束) 1.5 线程tid(pthread_self()) 1.6 线程局部存储(__thread) 1.7 线程分离(pthread_detach) 2. C的多线程 3. 笔试选择题 答…...

docker版本的Jenkins安装与更新技巧
因为jenkins/jenkins镜像默认带的jenkins版本比较低,导致安装完以后,很多插件因为版本问题无法安装。以下是最权威,最方便的安装教程。 1. 创建本地挂载目录 mkdir -p /mnt/dockerdata/jenkins/home/2. 修改挂载目录权限 chown -R 1000:10…...

[C++]3.类和对象下(this指针补充)+ 类和对象中构造函数和析构函数。
类和对象下(this指针补充) 类和对象中构造函数和析构函数 一.this补充:1.概念总结:2.两个问题: 二.构造函数和析构函数:一.类的默认构造:1.初始化和清理:2.拷贝复制:3.取…...

OpenLDAP LDIF详解
手把手一步步搭建LDAP服务器并加域 有必要理解的概念LDAPWindows Active Directory 服务器配置安装 OpenLDAP自定义安装修改对象(用户和分组等)修改olcSuffix 和 olcRootDN 属性增加olcRootPW 属性修改olcAccess属性验证新属性值 添加对象(用…...
Leetcode.33 搜索旋转排序数组
题目链接 Leetcode.33 搜索旋转排序数组 mid 题目描述 整数数组 n u m s nums nums 按升序排列,数组中的值 互不相同 。 在传递给函数之前, n u m s nums nums 在预先未知的某个下标 k ( 0 ≤ k < n u m s . l e n g t h )…...

ES 8.x 向量检索性能测试 把向量检索性能提升100倍!
向量检索不仅在的跨模态检索场景中应用广泛,随着chat gpt的或者,利用es的向量检索,在Ai领域发挥着越来越大的作用。 本文,主要测试es的向量检索性能。我从8.x就开始关注ES的向量检索了。当前ES已经发布到 8.10 版本。以下是官方文…...

云计算——ACA学习 云计算架构
作者简介:一名云计算网络运维人员、每天分享网络与运维的技术与干货。 公众号:网络豆云计算学堂 座右铭:低头赶路,敬事如仪 个人主页: 网络豆的主页 目录 写在前面 前期回顾 本期介绍 一.云计算架…...

基于深度学习实现一张单图,一个视频,一键换脸,Colab脚本使用方法,在线版本,普通人也可以上传一张图片体验机器学习一键换脸
基于深度学习实现一张单图,一个视频,一键换脸,Colab脚本使用方法,在线版本,普通人也可以上传一张图片体验机器学习一键换脸。 AI领域人才辈出,突然就跳出一个大佬“s0md3v”,开源了一个单图就可以进行视频换脸的项目。 项目主页给了一张换脸动图非常有说服力,真是一图…...

leetcode 21
递归的方式 class Solution { public:ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {if(l1 nullptr){return l2;}else if(l2 nullptr){return l1;}else if(l1->val < l2->val){l1->next mergeTwoLists(l1->next, l2);return l1;}else if(l1->va…...
【Spring Cloud】openfeign负载均衡方案(和lb发展历史)
文章目录 版本1:原始loadBalancerClient方案版本2:ribbon-loadbalancer方案版本3:openfeign方案(即**方案2openfeign版本**) 本文描述了Spring Cloud微服务中,各个服务间调用的负载均衡方案的升级历史&…...
R语言:主成分分析PCA
文章目录 主成分分析处理步骤数据集code主成分分析 主成分分析(或称主分量分析,principal component analysis)由皮尔逊(Pearson,1901)首先引入,后来被霍特林(Hotelling,1933)发展。 主成分分析是一种通过降维技术把多个变量化为少数几个主成分(即综合变量)的统计分…...
Linux下磁盘备份、文件备份和定时备份命令指南
文章目录 磁盘备份和定时备份命令指南1. 引言2. 磁盘备份命令dda. 简介和基本用法b. dd命令的参数和选项说明c. 使用dd命令进行磁盘镜像备份的步骤d. 恢复备份数据的方法和注意事项e. 示例:使用dd命令备份和还原磁盘镜像 3. 磁盘备份命令tara. 简介和基本用法b. tar…...

电脑软件:推荐一款非常强大的pdf阅读编辑软件
目录 一、软件简介 二、功能介绍 1、界面美观,打开速度快 2、可直接编辑pdf 3、非常强大好用的注释功能 4、很好用的页面组织和提取功能 5、PDF转word效果非常棒 6、强大的OCR功能 三、软件特色 四、软件下载 pdf是日常办公非常常见的文档格式,…...
Android 13.0 系统开机屏幕设置默认横屏显示
1.概述 在13.0的系统产品开发中,对于产品需求来说,由于是宽屏设备所以产品需要开机默认横屏显示,开机横屏显示这就需要从 两部分来实现,一部分是系统开机动画横屏显示,另一部分是系统屏幕显示横屏显示,从这两方面就可以做到开机默认横屏显示了 2.系统开机设置默认横屏显…...
Redis -- 基础知识1
1.介绍 1.初识Redis Redis:The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker. in-memory data:在内存中存储,Redis是在分布式系统中存储起作用的 解释&am…...

ubuntu 20.04 passwd 指令不能使用
Linux 更改用户密码报Changing password for user 用户名. passwd: Module is unknown或更改新增用户密码passwd:未知的用户名 报错信息如下: 解决方法: 可以排查 /etc/pam.d/passwd配置文件 注释掉包含pam_passwdqc.so模块的行,…...

单片机郭天祥(02)
1:解决keil5软件的乱码问题,修改编码为UTF-8 2:打开keil5使用debug对编写好的程序进行调试 给程序打上断点 使用仿真芯片 更改设备管理器相关设置 接通电源后点击debug连接到51单片机 使用stc-isp获取延时函数 将延时函数添加进入创建好的…...

Hadoop3教程(三十五):(生产调优篇)HDFS小文件优化与MR集群简单压测
文章目录 (168)HDFS小文件优化方法(169)MapReduce集群压测参考文献 (168)HDFS小文件优化方法 小文件的弊端,之前也讲过,一是大量占用NameNode的空间,二是会使得寻址速度…...

国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...

基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...

Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...
06 Deep learning神经网络编程基础 激活函数 --吴恩达
深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...

全志A40i android7.1 调试信息打印串口由uart0改为uart3
一,概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本:2014.07; Kernel版本:Linux-3.10; 二,Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01),并让boo…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...

GruntJS-前端自动化任务运行器从入门到实战
Grunt 完全指南:从入门到实战 一、Grunt 是什么? Grunt是一个基于 Node.js 的前端自动化任务运行器,主要用于自动化执行项目开发中重复性高的任务,例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...

代理服务器-LVS的3种模式与调度算法
作者介绍:简历上没有一个精通的运维工程师。请点击上方的蓝色《运维小路》关注我,下面的思维导图也是预计更新的内容和当前进度(不定时更新)。 我们上一章介绍了Web服务器,其中以Nginx为主,本章我们来讲解几个代理软件:…...