jacoco插桩源码,看这一篇就够了

知识储备

众所周知,jacoco的功能主要分成两块:

  • jacoco agent
  • jacoco cli

其中jacoco agent主要用来对业务方服务进行插装,而cli则提供一些工具对插桩数据进行处理,比如dump,merge,report等,今天我们着重通过源码来分析jacoco的插桩过程,在分析其插桩逻辑前,我们需要一定的知识储备,主要包括但不限于

ASM,Java Instrumentation等

Java ASM是一个Java字节码操控和分析框架。它能够用于动态生成代码,动态修改已有代码,执行静态代码分析等操作。ASM提供了一些基础的字节码转换和处理工具,开发人员可以在此基础上进行扩展,构建出复杂的编程工具和技术。

ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 从使用者的角度来看是数据(它们以 .class 文件的形式存在于磁盘上),但是从生产者或者消费者(比如类加载器)的角度来看,它就是一种产品。ASM 提供了一些类,以使 Java class 从类文件转移到内存中的过程中,可以“加工”这个产品。

Java ASM 的优点在于它功能强大并且高效,但是由于直接面对的是字节码,所以使用起来也比较复杂

简而言之,ASM就是一个对java字节码进行操作的工具,其核心功能主要采用访问者模式,对类、方法、属性、指令等进行遍历,然后访问或者修改。

Instrumentation是基于JVMTI技术,JVMTI代表Java Virtual Machine Tools Interface,是Java虚拟机提供的一组原生接口,供开发者构建各种工具和实用程序,如监控工具、调试器、分析器等。

JVMTI具有强大的能力,包括但不限于:

  1. 检查和修改类和对象的状态。
  2. 监视线程的创建和销毁。
  3. 监视类的加载和卸载。
  4. 监视虚拟机的垃圾回收。
  5. 设置断点和监听各种事件。
  6. 获取方法调用的栈轨迹。

JVMTI是Java虚拟机的一部分,因此可以在Java程序执行的任何阶段进行操作,包括启动阶段、运行阶段和退出阶段。

尽管JVMTI具有强大的功能,但由于它是一种底层接口,所以使用起来可能比较复杂,需要深入理解虚拟机和字节码的工作原理。而且,错误的使用可能会导致程序崩溃或其他未预期的行为。因此,建议只在开发调试和监控工具等专业领域使用JVMTI。

Java Instrumentation API是Java SE 5.0引入的一种强大的工具,可以用来修改已加载到JVM中的类的字节码。这个API允许Java开发人员在运行时查看和修改类和对象的状态。

Java Instrumentation API常用于一些高级的开发任务,比如性能监控、分析和优化、代码覆盖率分析、故障排查等等。例如,开发人员可以使用Java Instrumentation来监视程序的内存使用情况,或者插入额外的代码来追踪和记录方法的调用情况。

要使用Java Instrumentation,你需要创建一个特殊的"agent"类,并且在JVM启动时通过特定的命令行参数将其加载到JVM中。这个agent类需要实现特定的接口,并且可以定义一些premain或者agentmain方法来进行初始化工作。

虽然Java Instrumentation是一个强大的工具,但是也需要谨慎使用。修改运行时的类可能会引入一些不可预见的行为,甚至可能导致程序崩溃。因此,最好只在明确知道自己在做什么的情况下使用它。

没错,jacoco的agent能附着在业务服务上运行,其使用的就是Instrumentation,那么,我们首先来了解一下Instrumentation的使用方法

  • 首先,你需要创建一个Agent类,它应该包含一个名为premain的静态方法。JVM会在启动时调用这个方法,并通过该方法的参数提供一个Instrumentation对象:
public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, 
                                    Class<?> classBeingRedefined, 
                                    ProtectionDomain protectionDomain, 
                                    byte[] classfileBuffer) throws IllegalClassFormatException {
                // 使用ASM等字节码操作工具对classfileBuffer进行修改,
                // 例如插入在方法调用前后打印消息的代码。
                // 假设你已经有一个modifyByteCode方法来做这个工作:
                return modifyByteCode(classfileBuffer);
            }
        });
    }
}
  • 然后,你需要在JVM启动时指定你的Agent类。这可以通过-javaagent命令行参数实现:
java -javaagent:myagent.jar MyApplication

其中,myagent.jar是包含你的Agent类的JAR文件,MyApplication是你的应用程序的主类。请注意,你的JAR文件需要包含一个名为MANIFEST.MF的清单文件,其中应该包含一个Premain-Class属性来指定你的Agent类:

Premain-Class: MyAgent

在以上的示例中ClassFileTransformer是一个接口,实现这个接口,然后注册到Instrumentation上去,当类被加载到内存中的时候就会触发其transform方法,我们就可以在这个阶段对字节码进行修改,从而达到我们的目的。是不是就像观察者模式。

jacoco agent 源码分析

在这里插入图片描述

jacoco整个插桩过程如上图所示,PreMain注册了CoverageTransformer,当类加载到jvm的时候就会触达调用instrumenter方法,然后Instrumenter方法调用了类Instrumenter的instrument方法, 类Instrumenter的instrument方法调用了类ProbeArrayStrategyFactory的createFor方法,然后根据不同的jdk版本生成不同的策略,然后类Instrumenter的instrument方法调用了类ClassProbesAdapter的visitMethod方法, 接着类ClassProbesAdapter的visitMethod方法调用了类MethodInstrumenter的visitMethod方法, 类MethodInstrumenter的visitMethod方法调用了类ProbeInserter的insertProbe方法 类ProbeInserter的insertProbe方法调用了类ClassFieldProbeArrayStrategy的storeInstance方法 ClassProbesAdapte调用了ClassFieldProbeArrayStrategy的addMembers方法 完成了整个类的插桩。
看了上面的图是不是感觉有点绕,没关系我们根据源码逐步来分析。首先,我们指定了preMain的入口
在这里插入图片描述

	public static void premain(final String options, final Instrumentation inst)
			throws Exception {
        //解析参数
		final AgentOptions agentOptions = new AgentOptions(options);
        //创建Agent实例
		final Agent agent = Agent.getInstance(agentOptions);
        //创建Runtime实例
		final IRuntime runtime = createRuntime(inst);
		//启动Runtime
		runtime.startup(agent.getData());
		//这里把自定义的CoverageTransformer加入到了inst的transformer列表中,即注册,当类加载的时候会回调CoverageTransformer里的instrument方法
		inst.addTransformer(new CoverageTransformer(runtime, agentOptions,
				IExceptionLogger.SYSTEM_ERR));
	}

CoverageTransformer实现了ClassFileTransformer接口,当类加载的时候会回调CoverageTransformer里的instrument方法

	public byte[] transform(final ClassLoader loader, final String classname,
			final Class<?> classBeingRedefined,
			final ProtectionDomain protectionDomain,
			final byte[] classfileBuffer) throws IllegalClassFormatException {

		// We do not support class retransformation:
		if (classBeingRedefined != null) {
			return null;
		}
        //过滤掉不需要插桩的类
		if (!filter(loader, classname, protectionDomain)) {
			return null;
		}

		try {
			//读取class类文件
			classFileDumper.dump(classname, classfileBuffer);
			//对class类文件进行字节码插桩,然后返回插桩后的字节码
			return instrumenter.instrument(classfileBuffer, classname);
		} catch (final Exception ex) {
			final IllegalClassFormatException wrapper = new IllegalClassFormatException(
					ex.getMessage());
			wrapper.initCause(ex);
			// Report this, as the exception is ignored by the JVM:
			logger.logExeption(wrapper);
			throw wrapper;
		}
	}

接着我们看Instrumenter类的instrument方法

	private byte[] instrument(final byte[] source) {
		final long classId = CRC64.classId(source);
		//创建ClassReader
		final ClassReader reader = InstrSupport.classReaderFor(source);
		//创建ClassWriter
		final ClassWriter writer = new ClassWriter(reader, 0) {
			@Override
			protected String getCommonSuperClass(final String type1,
					final String type2) {
				throw new IllegalStateException();
			}
		};
		//主要根据不同jdk版本生成不同的策略
		final IProbeArrayStrategy strategy = ProbeArrayStrategyFactory
				.createFor(classId, reader, accessorGenerator);
		final int version = InstrSupport.getMajorVersion(reader);
		final ClassVisitor visitor = new ClassProbesAdapter(
				new ClassInstrumenter(strategy, writer),
				InstrSupport.needsFrames(version));
		//遍历类文件进行插桩
		reader.accept(visitor, ClassReader.EXPAND_FRAMES);
		return writer.toByteArray();
	}

整体过程就是对class字节码进行读取,然后遍历修改,最后再写回,ProbeArrayStrategyFactory是一个探针数组策略工厂,根据不同的jdk版本生成不同的策略,正常情况下我们使用的都是ClassFieldProbeArrayStrategy策略
根据上面的代码,生成探针数组策略后,会创建一个ClassProbesAdapter,这个类是一个适配器,它主要适配了ClassInstrumenter类和ClassAnalyzer类(用来报告生成),适配器主要做了两件事,一个是遍历类的每个方法,一个是在方法结束时统计探针数量
这里需要注意,假设我们的原始类是这样

public class Example {
    private final String message;

    public Example(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

那么经过插桩后的类是这样的

public class Example {
    private final String message;
    private static transient final boolean[] $jacocoData;

    public Example(String message) {
        boolean[] arr = $jacocoInit();
        super();
        arr[0] = true;
        this.message = message;
        arr[1] = true;
    }

    public String getMessage() {
        $jacocoData[2] = true;
        return message;
    }

    private static boolean[] $jacocoInit() {
        boolean[] arr = new boolean[3];
        $jacocoData = arr;
        return arr;
    }
}

插桩有几个部分,首先是添加了属性字段 j a c o c o D a t a ,然后添加了初始化探针方法 jacocoData,然后添加了初始化探针方法 jacocoData,然后添加了初始化探针方法jacocoInit(),最后是对每个方法进行插桩,那个每个方法需要插多少个桩呢
jacoco agent的操作是,先对每个方法进行插桩,同时统计探针的数量,然后最后再类访问结束的时候,插入了属性和探针初始化方法
所有我们主要看ClassProbesAdapter类的visitMethod方法调用了

		final MethodProbesVisitor mv = cv.visitMethod(access, name, desc,
				signature, exceptions);

和类访问结束时调用了

	public void visitEnd() {
		cv.visitTotalProbeCount(counter);
		super.visitEnd();
	}

我们知道这里的cv在插桩阶段其实就是我们的ClassInstrumenter类
那么,我们再来看看ClassInstrumenter类的visitMethod方法和visitTotalProbeCount方法

public MethodProbesVisitor visitMethod(final int access, final String name,
			final String desc, final String signature,
			final String[] exceptions) {

		InstrSupport.assertNotInstrumented(name, className);

		final MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
				exceptions);

		if (mv == null) {
			return null;
		}
		final MethodVisitor frameEliminator = new DuplicateFrameEliminator(mv);
		final ProbeInserter probeVariableInserter = new ProbeInserter(access,
				name, desc, frameEliminator, probeArrayStrategy);
		return new MethodInstrumenter(probeVariableInserter,
				probeVariableInserter);
	}

这里主要是创建了一个MethodInstrumenter,其包含了一个ProbeInserter探针插入器,而visitTotalProbeCount方法则是对属性和初始化方法的插入

   	public void visitTotalProbeCount(final int count) {
		probeArrayStrategy.addMembers(cv, count);
	}

看到这里,我们都还没有对探针进行操作,但是我们知道了两个核心类,一个是probeArrayStrategy,一个是ProbeInserter,ProbeInserter是针对每个方法进行探针插入的核心类,probeArrayStrategy是对类属性和初始化方法进行插入的核心类
MethodInstrumenter的方法调用入口同样是在ClassProbesAdapter类的visitMethod方法中

	final MethodProbesAdapter probesAdapter = new MethodProbesAdapter(
						methodProbes, ClassProbesAdapter.this);
				if (trackFrames) {
					final AnalyzerAdapter analyzer = new AnalyzerAdapter(
							ClassProbesAdapter.this.name, access, name, desc,
							probesAdapter);
					probesAdapter.setAnalyzer(analyzer);
					methodProbes.accept(this, analyzer);
				} else {
					// 这里调用的就是mv的钩子方法
					methodProbes.accept(this, probesAdapter);
				}

这里调用了我们从ClassInstrumenter返回的MethodProbesAdapter,MethodProbesAdapter同样是一个适配器,其插桩过程中的核心适配是MethodInstrumenter类
MethodProbesAdapter类的遍历中有个核心方法

	/**
	 * 访问标签,Label是字节码中的位置指示,它可以作为跳转指令的目标位置,也可以用于标记异常处理区块的开始和结束等。
	 *
	 * @param label 标签
	 */
	@Override
	public void visitLabel(final Label label) {
		if (LabelInfo.needsProbe(label)) {
			if (tryCatchProbeLabels.containsKey(label)) {
				probesVisitor.visitLabel(tryCatchProbeLabels.get(label));
			}
			probesVisitor.visitProbe(idGenerator.nextId());
		}
		probesVisitor.visitLabel(label);
	}
	
	
	@Override
	public void visitJumpInsn(final int opcode, final Label label) {
		if (LabelInfo.isMultiTarget(label)) {
			probesVisitor.visitJumpInsnWithProbe(opcode, label,
					idGenerator.nextId(), frame(jumpPopCount(opcode)));
		} else {
			probesVisitor.visitJumpInsn(opcode, label);
		}
	}
	
	@Override
	public void visitJumpInsn(final int opcode, final Label label) {
		if (LabelInfo.isMultiTarget(label)) {
			probesVisitor.visitJumpInsnWithProbe(opcode, label,
					idGenerator.nextId(), frame(jumpPopCount(opcode)));
		} else {
			probesVisitor.visitJumpInsn(opcode, label);
		}
	}
	

这里三个方法分别会调用MethodInstrumenter类的下面几个方法

	/**
	 * 无条件插入探针
	 *
	 * @param probeId 调查id
	 */
	@Override
	public void visitProbe(final int probeId) {
		probeInserter.insertProbe(probeId);
	}

	/**
	 * 在返回指令前插入探针
	 *
	 * @param opcode  操作码
	 * @param probeId 调查id
	 */
	@Override
	public void visitInsnWithProbe(final int opcode, final int probeId) {
		probeInserter.insertProbe(probeId);
		mv.visitInsn(opcode);
	}

	/**
	 * 在跳转指令(比如if语句)前插入探针
	 *
	 * @param opcode  操作码
	 * @param label   标签
	 * @param probeId 调查id
	 * @param frame   框架
	 */
	@Override
	public void visitJumpInsnWithProbe(final int opcode, final Label label,
			final int probeId, final IFrame frame) {
		if (opcode == Opcodes.GOTO) {
			probeInserter.insertProbe(probeId);
			mv.visitJumpInsn(Opcodes.GOTO, label);
		} else {
			final Label intermediate = new Label();
			mv.visitJumpInsn(getInverted(opcode), intermediate);
			probeInserter.insertProbe(probeId);
			mv.visitJumpInsn(Opcodes.GOTO, label);
			mv.visitLabel(intermediate);
			frame.accept(mv);
		}
	}

概括一句就是会对方法起始行、语句跳转(if/switch)、语句返回前插入探针
具体的插桩是ProbeInserter类的insertProbe方法

public void insertProbe(final int id) {

		// For a probe we set the corresponding position in the boolean[] array
		// to true.

		mv.visitVarInsn(Opcodes.ALOAD, variable);

		// Stack[0]: [Z

		InstrSupport.push(mv, id);

		// Stack[1]: I
		// Stack[0]: [Z

		mv.visitInsn(Opcodes.ICONST_1);

		// Stack[2]: I
		// Stack[1]: I
		// Stack[0]: [Z

		mv.visitInsn(Opcodes.BASTORE);
	}
}

获取你看不懂,我这边给个简单的例子

插桩前代码
 public void simpleMethod() {
    // 这是我们要插入探针的地方
    System.out.println("Hello World");
}
插桩前指令
0: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc           #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

插桩后指令
0: aload_0               // 加载探针数组到操作数栈
1: iconst_0              // 将探针id(此处为0)压入操作数栈
2: iconst_1              // 将数值1压入操作数栈
3: bastore               // 将栈顶的数值1存储到探针数组的第0个位置
4: getstatic     #2      // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc           #3      // String Hello World
9: invokevirtual #4      // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return

插桩后反编译
boolean[] probes = new boolean[1];  // 假设只有一个探针
public void simpleMethod() {
    probes[0] = true;  // 插入的探针
    System.out.println("Hello World");
}

我们还要关注ProbeInserter类的visitCode方法

	@Override
	public void visitCode() {
		accessorStackSize = arrayStrategy.storeInstance(mv, clinit, variable);
		mv.visitCode();
	}

它在方法开始前就会调用ClassFieldProbeArrayStrategy类的storeInstance方法

  public int storeInstance(final MethodVisitor mv, final boolean clinit,
			final int variable) {
		mv.visitMethodInsn(Opcodes.INVOKESTATIC, className,
				InstrSupport.INITMETHOD_NAME, InstrSupport.INITMETHOD_DESC,
				false);
		mv.visitVarInsn(Opcodes.ASTORE, variable);
		return 1;
	}

其实它就干了一件事,在方法开始行调用 $jacocoInit()方法,所以刚刚的示例插桩后应该是这样的

boolean[] probes = new boolean[1];  // 假设只有一个探针
public void simpleMethod() {
    $jacocoInit(); // 插入的探针初始化方法
    probes[0] = true;  // 插入的探针
    System.out.println("Hello World");
}

$jacocoInit()是在访问方法开始就插入的,probes[0] = true是在方法访问过程中插入的,这样就保证了探针数组的初始化和探针的插入顺序
ClassProbesAdapter类有个计数器,方法每插入一个探针,就会+1,汇总探针总数

public int nextId() {
return counter++;
}

可以看到MethodInstrumenter的visitLabel方法这里调用了之前的nextId方法

probesVisitor.visitProbe(idGenerator.nextId());

到这里我们已经了解完了jacoco对方法的插桩,下面我们再来分析,
ClassInstrumenter的visitTotalProbeCount方法调用了ClassFieldProbeArrayStrategy的addMembers方法

    public void addMembers(final ClassVisitor cv, final int probeCount) {
		createDataField(cv);
		createInitMethod(cv, probeCount);
	}

	private void createDataField(final ClassVisitor cv) {
		cv.visitField(InstrSupport.DATAFIELD_ACC, InstrSupport.DATAFIELD_NAME,
				InstrSupport.DATAFIELD_DESC, null, null);
	}

	private void createInitMethod(final ClassVisitor cv, final int probeCount) {
		final MethodVisitor mv = cv.visitMethod(InstrSupport.INITMETHOD_ACC,
				InstrSupport.INITMETHOD_NAME, InstrSupport.INITMETHOD_DESC,
				null, null);
		mv.visitCode();

		// Load the value of the static data field:
		mv.visitFieldInsn(Opcodes.GETSTATIC, className,
				InstrSupport.DATAFIELD_NAME, InstrSupport.DATAFIELD_DESC);
		mv.visitInsn(Opcodes.DUP);

		// Stack[1]: [Z
		// Stack[0]: [Z

		// Skip initialization when we already have a data array:
		final Label alreadyInitialized = new Label();
		mv.visitJumpInsn(Opcodes.IFNONNULL, alreadyInitialized);

		// Stack[0]: [Z

		mv.visitInsn(Opcodes.POP);
		final int size = genInitializeDataField(mv, probeCount);

		// Stack[0]: [Z

		// Return the class' probe array:
		if (withFrames) {
			mv.visitFrame(Opcodes.F_NEW, 0, FRAME_LOCALS_EMPTY, 1,
					FRAME_STACK_ARRZ);
		}
		mv.visitLabel(alreadyInitialized);
		mv.visitInsn(Opcodes.ARETURN);

		mv.visitMaxs(Math.max(size, 2), 0); // Maximum local stack size is 2
		mv.visitEnd();
	}

这里就是对类添加 j a c o c o D a t a 和初始化探针方法 jacocoData和初始化探针方法 jacocoData和初始化探针方法jacocoInit()的地方,至此,这个类的插桩过程就结束了,整体来说ClassProbesAdapte是一个非常核心的如果类,作为一个适配器,它不仅是整个插桩逻辑的入口,它同样是生成报告的入口,生成报告的时候,我们传递ClassAnalyzer类,然后再适配MethodAnalyzer类,在弹出插入的位置还原exec探针数据,从而拿到指令覆盖率,方法覆盖率,行覆盖率等数据,最后生成报告
了解jacoco的源码后我们就可以对插桩逻辑进行干预,比我我们不止想只存储探针数据,还想存储额外信息,就可以改变插桩的逻辑实现丰富化的结构等等功能。

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

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

相关文章

计算机毕设 大数据工作岗位数据分析与可视化 - python flask

文章目录 0 前言1 课题背景2 实现效果3 项目实现3.1 概括 3.2 Flask实现3.3 HTML页面交互及Jinja2 4 **完整代码**5 最后 0 前言 &#x1f525; 这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的毕设题目缺少创新和亮点&#xff0c;往往达不到毕业答辩的要…

作用域插槽slot-scope

一般用于组件封装&#xff0c;将使用props传入组件的数据再次调出来或者单纯调用组件中的数据。也可用于为组件某个部分自定义样式以及为某次使用组件自定义样式。 直接拿elementui的el-table举例&#xff1a; <template><el-table v-loading"loading&q…

55. 右旋字符串(第八期模拟笔试)

55. 右旋字符串&#xff08;第八期模拟笔试&#xff09; 原题链接&#xff1a;完成情况&#xff1a;解题思路&#xff1a;参考代码&#xff1a;错误经验吸取 原题链接&#xff1a; 55. 右旋字符串&#xff08;第八期模拟笔试&#xff09; https://kamacoder.com/problempage…

spring6-国际化:i18n | 数据校验:Validation

文章目录 1、国际化&#xff1a;i18n1.1、i18n概述1.2、Java国际化1.3、Spring6国际化1.3.1、MessageSource接口1.3.2、使用Spring6国际化 2、数据校验&#xff1a;Validation2.1、Spring Validation概述2.2、实验一&#xff1a;通过Validator接口实现2.3、实验二&#xff1a;B…

伦敦金冬令时开市时间怎样调整

在刚刚过去的一周&#xff0c;欧美的金融市场已经正式进入了冬令时&#xff0c;这对伦敦金市场的交易时间也产生了影响。由于美国于今年11月5日(星期日&#xff09;开始正式实施冬令时间&#xff0c;所以香港的伦敦金平台的交易时间也随之而有所调整。 从今年11月6日开始&#…

体力属性在重生奇迹MU中的演变史

我相信&#xff0c;在任何一个网络游戏中都有体力这种属性&#xff0c;它本身就是血量的另一种表达&#xff0c;先天体力有优势的职业&#xff0c;往往就是后期成长较高&#xff0c;这已经是网游中一种常态&#xff0c;因为高血在PK中占据优势&#xff01;重生奇迹MU同样如此&a…

FiRa标准——MAC实现(二)

在IEEE 802.15.4z标准中&#xff0c;最关键的就是引入了STS&#xff08;加扰时间戳序列&#xff09;&#xff0c;实现了安全测距&#xff0c;大大提高了测距应用的安全性能。在FiRa的实现中&#xff0c;其密钥派生功能是非常重要的一个部分&#xff0c;本文首先对FiRa MAC中加密…

Haproxy实现七层负载均衡

目录 Haproxy概述 haproxy算法&#xff1a; Haproxy实现七层负载 ①部署nginx-server测试页面 ②(主/备)部署负载均衡器 ③部署keepalived高可用 ④增加对haproxy健康检查 ⑤测试 Haproxy概述 haproxy---主要是做负载均衡的7层&#xff0c;也可以做4层负载均衡 apache也可…

光刻掩膜版怎么制作的?

光掩膜版基本上是 IC 设计的“主模板”。掩模版有不同的尺寸。常见尺寸为 6 x 6 英寸一般的掩膜版由石英或玻璃基板组成。光掩膜版涂有不透明薄膜。更复杂的掩模版使用其他材料。 一般来说&#xff0c;术语“photo mask”用于描述与 1X 步进机或光刻系统一起使用的“主模板”。…

Xmind常用快捷键

Xmind 是什么&#xff1f; Xmind 是一款全功能的思维导图和头脑风暴软件。像大脑的瑞士军刀一般&#xff0c;助你理清思路&#xff0c;捕捉创意。 全功能&#xff1a;提供9种专业的的思维导图结构&#xff0c;丰富的模板和配色&#xff0c;精美的贴纸和插画&#xff0c;还有演…

ansible-第二天

ansible 第二天 以上学习了ping、command、shell、script模块&#xff0c;但一般不建议使用以上三个&#xff0c;因为这三个模块没有幂等性。举例如下&#xff1a; [rootcontrol ansible]# ansible test -a "mkdir /tmp/1234"[WARNING]: Consider using the file …

RabbitMQ 之 Work Queues 工作队列

目录 一、轮训分发消息 1、抽取工具类 2、启动两个工作线程 3、生产者代码 4、结果展示 二、消息应答 1、概念 2、自动应答 3、消息应答的方法 4、Multiple 的解释 5、消息自动重新入队 6、消息手动应答代码 &#xff08;1&#xff09;生产者 &#xff08;2&#…

上拉电阻与下拉电阻

文章目录 上拉电阻下拉电阻上下拉电阻作用1、稳定信号2、减少电磁干扰3、提高驱动能力 大家在玩单片机的过程中&#xff0c;一定没少听过上拉电阻和下拉电阻这组名词&#xff0c;那么到底什么是上拉电阻和下拉电阻呢&#xff1f;今天我们一起来简单了解一下 上拉电阻 上拉电阻…

企业如何解决被“薅羊毛”

随着互联网的普及和电子商务的兴起&#xff0c;越来越多的消费者选择在线购物。然而&#xff0c;一些消费者可能会利用企业的促销活动或优惠券来获取额外优惠&#xff0c;甚至恶意攻击企业的营销资金。这种行为被形象地称为“薅羊毛”。 对于企业而言&#xff0c;如何解决被“薅…

信通院发布的 “信息系统稳定性保障能力建设指南” 有点干货

刚刚看了信息系统稳定性实验室、中国信息通信研究院云计算与大数据研究所联合发布的 信息系统稳定性保障能力建设指南&#xff0c;感觉还是有点干货的。 节选如下&#xff1a; 概括的比较全 关键指标 行业案例 免费在线阅读和下载地址&#xff1a;信息系统稳定性保障能力建设指…

【Python小练手】使用PySimpleGUI和Pygame创作一个MP3播放器(附完整代码)

文章目录 前言一、来说说思路&#xff08;文心一言提供&#xff09;二、完整代码&#xff08;参考文心&#xff0c;自行修改&#xff09;总结附录 前言 闲来无事&#xff0c;做了MP3播放器练练手&#xff0c;主要是研究下PySimpleGUI的界面窗口设计。先上图&#xff0c;一睹为…

Leetcode刷题详解——电话号码的字母组合

1. 题目链接&#xff1a;17. 电话号码的字母组合 2. 题目描述&#xff1a; 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。…

对于线程的收尾

一)对于synchronized的锁策略: synchronzed是一个自适应的锁&#xff0c;应该根据具体情况来决定选取那种锁策略&#xff1b; 1)synchronized既是一个乐观锁又是一个悲观锁&#xff0c;一开始是一个乐观锁&#xff0c;但是如果发现锁冲突的概率比较高&#xff0c;就会自动转化成…

操作系统实验二、进程和线程管理(Windows 2学时)单线程创建(有详细代码解释和运行步骤)

实验二、进程和线程管理(Windows 2学时) 一、实验目的 通过实验使学生进一步了解进程、进程状态、进程控制等基本概念。基本能达到下列具体的目标: 理解进程 PCB 的概念,以及 PCB 如何实现、如何组织以及管理。加深对进程和线程概念的理解,进一步认识并发执行的本质。掌握…

登录注册代码模板(Vue3+SpringBoot)[邮箱发送验证码(HTML)、RSA 加密解密(支持长文本)、黑暗与亮色主题切换、AOP信息校验]

文章归档&#xff1a;https://www.yuque.com/u27599042/coding_star/cx5ptule64utcr9e 仓库地址 https://gitee.com/tongchaowei/login-register-template 网页效果展示 相关说明 在该代码模板中&#xff0c;实现了如下功能&#xff1a; 邮箱发送验证码&#xff08;邮件内容…