字节码调教的入口 —— JVM 的寄生插件 javaagent 那些事
Java Instrumentation 包
Java Instrumentation 概述
Java Instrumentation 这个技术看起来非常神秘,很少有书会详细介绍。但是有很多工具是基于 Instrumentation 来实现的:
-
APM 产品: pinpoint、skywalking、newrelic、听云的 APM 产品等都基于 Instrumentation 实现 -
热部署工具:Intellij idea 的 HotSwap、Jrebel 等 -
Java 诊断工具:Arthas、Btrace 等
由于对字节码修改功能的巨大需求,JDK 从 JDK5 版本开始引入了java.lang.instrument
包。它可以通过 addTransformer 方法设置一个 ClassFileTransformer,可以在这个 ClassFileTransformer 实现类的转换。
JDK 1.5 支持静态 Instrumentation,基本的思路是在 JVM 启动的时候添加一个代理(javaagent),每个代理是一个 jar 包,其 MANIFEST.MF 文件里指定了代理类,这个代理类包含一个 premain 方法。JVM 在类加载时候会先执行代理类的 premain 方法,再执行 Java 程序本身的 main 方法,这就是 premain 名字的来源。在 premain 方法中可以对加载前的 class 文件进行修改。这种机制可以认为是虚拟机级别的 AOP,无需对原有应用做任何修改,就可以实现类的动态修改和增强。
从 JDK 1.6 开始支持更加强大的动态 Instrument,在JVM 启动后通过 Attach API 远程加载,后面会详细介绍。
本文会分为 javaagent 和动态 Attach 两个部分来介绍
Java Instrumentation 核心方法
Instrumentation 是 java.lang.instrument
包下的一个接口,这个接口的方法提供了注册类文件转换器、获取所有已加载的类等功能,允许我们在对已加载和未加载的类进行修改,实现 AOP、性能监控等功能。
常用的方法如下:
/**
* 为 Instrumentation 注册一个类文件转换器,可以修改读取类文件字节码
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
/**
* 对JVM已经加载的类重新触发类加载
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/**
* 获取当前 JVM 加载的所有类对象
*/
Class[] getAllLoadedClasses()
它的 addTransformer 给 Instrumentation 注册一个 transformer,transformer 是 ClassFileTransformer 接口的实例,这个接口就只有一个 transform 方法,调用 addTransformer 设置 transformer 以后,后续JVM 加载所有类之前都会被这个 transform 方法拦截,这个方法接收原类文件的字节数组,返回转换过的字节数组,在这个方法中可以做任意的类文件改写。
下面是一个空的 ClassFileTransformer 的实现:
public class MyClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classBytes) throws IllegalClassFormatException {
// 在这里读取、转换类文件
return classBytes;
}
}
接下来我们来介绍本文的主角之一 javaagent。
Javaagent 介绍
Javaagent 是一个特殊的 jar 包,它并不能单独启动的,而必须依附于一个 JVM 进程,可以看作是 JVM 的一个寄生插件,使用 Instrumentation 的 API 用来读取和改写当前 JVM 的类文件。
Agent 的两种使用方式
它有两种使用方式:
-
在 JVM 启动的时候加载,通过 javaagent 启动参数 java -javaagent:myagent.jar MyMain,这种方式在程序 main 方法执行之前执行 agent 中的 premain 方法 -
在 JVM 启动后 Attach,通过 Attach API 进行加载,这种方式会在 agent 加载以后执行 agentmain 方法 premain 和 agentmain 方法签名如下:
public static void premain(String agentArgument, Instrumentation instrumentation) throws Exception
public static void agentmain(String agentArgument, Instrumentation instrumentation) throws Exception
这两个方法都有两个参数
-
第一个 agentArgument 是 agent 的启动参数,可以在 JVM 启动命令行中设置,比如
java -javaagent:<jarfile>=appId:agent-demo,agentType:singleJar test.jar
的情况下 agentArgument 的值为 "appId:agent-demo,agentType:singleJar"。 -
第二个 instrumentation 是 java.lang.instrument.Instrumentation 的实例,可以通过 addTransformer 方法设置一个 ClassFileTransformer。
第一种 premain 方式的加载时序如下:
Agent 打包
为了能够以 javaagent 的方式运行 premain 和 agentmain 方法,我们需要将其打包成 jar 包,并在其中的 MANIFEST.MF 配置文件中,指定 Premain-class 等信息,一个典型的生成好的 MANIFEST.MF 内容如下
为了能够以 javaagent 的方式运行 premain 和 agentmain 方法,我们需要将其打包成 jar 包,并在其中的 MANIFEST.MF 配置文件中,指定 Premain-class 等信息,一个典型的生成好的 MANIFEST.MF 内容如下
下面是一个可以帮助生成上面 MANIFEST.MF 的 maven 配置
<build>
<finalName>my-javaagent</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>me.geek01.javaagent.AgentMain</Agent-Class>
<Premain-Class>me.geek01.javaagent.AgentMain</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
Agent 使用方式一:JVM 启动参数
下面使用 javaagent 实现简单的函数调用栈跟踪,以下面的代码为例:
public class MyTest {
public static void main(String[] args) {
new MyTest().foo();
}
public void foo() {
bar1();
bar2();
}
public void bar1() {
}
public void bar2() {
}
}
通过 javaagent 启动参数的方式在每个函数进入和结束时都打印一行日志,实现调用过程的追踪的效果。
核心的方法 instrument 的逻辑如下:
public static class MyMethodVisitor extends AdviceAdapter {
@Override
protected void onMethodEnter() {
// 在方法开始处插入 <<<enter xxx
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("<<<enter " + this.getName());
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
super.onMethodEnter();
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
// 在方法结束处插入 <<<exit xxx
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn(">>>exit " + this.getName());
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
把 agent 打包生成 my-trace-agent.jar,添加 agent 启动 MyTest 类
java -javaagent:/path_to/my-trace-agent.jar MyTest
可以看到输出结果如下:
<<<enter main
<<<enter foo
<<<enter bar1
>>>exit bar1
<<<enter bar2
>>>exit bar2
>>>exit foo
>>>exit main
通过上面的方式,我们在不修改 MyTest 类源码的情况下实现了调用链跟踪的效果。更加健壮和完善的调用链跟踪实现会在后面的 APM 章节详细介绍。
Agent 使用方式二:Attach API 使用
在 JDK5 中,开发者只能 JVM 启动时指定一个 javaagent 在 premain 中操作字节码,Instrumentation 也仅限于 main 函数执行前,这样的方式存在一定的局限性。从 JDK6 开始引入了动态 Attach Agent 的方案,除了在命令行中指定 javaagent,现在可以通过 Attach API 远程加载。我们常用的 jstack、arthas 等工具都是通过 Attach 机制实现的。
接下来我们会结合跨进程通信中的信号和 Unix 域套接字来看 JVM Attach API 的实现原理
JVM Attach API 基本使用
下面以一个实际的例子来演示动态 Attach API 的使用,代码中有一个 main 方法,每隔 3s 输出 foo 方法的返回值 100,接下来动态 Attach 上 MyTestMain 进程,修改 foo 的字节码,让 foo 方法返回 50。
public class MyTestMain {
public static void main(String[] args) throws InterruptedException {
while (true) {
System.out.println(foo());
TimeUnit.SECONDS.sleep(3);
}
}
public static int foo() {
return 100; // 修改后 return 50;
}
}
步骤如下:
1、编写 Attach Agent,对 foo 方法做注入,完整的代码见:github.com/arthur-zhan…
动态 Attach 的 agent 与通过 JVM 启动 javaagent 参数指定的 agent jar 包的方式有所不同,动态 Attach 的 agent 会执行 agentmain 方法,而不是 premain 方法。
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
System.out.println("agentmain called");
inst.addTransformer(new MyClassFileTransformer(), true);
Class classes[] = inst.getAllLoadedClasses();
for (int i = 0; i < classes.length; i++) {
if (classes[i].getName().equals("MyTestMain")) {
System.out.println("Reloading: " + classes[i].getName());
inst.retransformClasses(classes[i]);
break;
}
}
}
}
2、因为是跨进程通信,Attach 的发起端是一个独立的 java 程序,这个 java 程序会调用 VirtualMachine.attach 方法开始和目标 JVM 进行跨进程通信。
public class MyAttachMain {
public static void main(String[] args) throws Exception {
VirtualMachine vm = VirtualMachine.attach(args[0]);
try {
vm.loadAgent("/path/to/agent.jar");
} finally {
vm.detach();
}
}
}
使用 jps 查询到 MyTestMain 的进程 id,
java -cp /path/to/your/tools.jar:. MyAttachMain pid
可以看到 MyTestMain 的输出的 foo 方法已经返回了 50。
java -cp . MyTestMain
100
100
100
agentmain called
Reloading: MyTestMain
50
50
50
JVM Attach API 的底层原理
JVM Attach API 的实现主要基于信号和 Unix 域套接字,接下来详细介绍这两部分的内容。
信号是什么
信号是某事件发生时对进程的通知机制,也被称为“软件中断”。信号可以看做是一种非常轻量级的进程间通信,信号由一个进程发送给另外一个进程,只不过是经由内核作为一个中间人发出,信号最初的目的是用来指定杀死进程的不同方式。
每个信号都有一个名字,以 "SIG" 开头,最熟知的信号应该是 SIGINT
,我们在终端执行某个应用程序的过程中按下 Ctrl+C
一般会终止正在执行的进程,正是因为按下 Ctrl+C
会发送 SIGINT
信号给目标程序。
每个信号都有一个唯一的数字标识,从 1 开始,下面是常见的信号量列表:
在 Linux 中,一个前台进程可以使用 Ctrl+C 进行终止,对于后台进程需要使用 kill 加进程号的方式来终止,kill 命令是通过发送信号给目标进程来实现终止进程的功能。默认情况下,kill 命令发送的是编号为 15 的 SIGTERM 信号,这个信号可以被进程捕获,选择忽略或正常退出。目标进程如果没有自定义处理这个信号,就会被终止。对于那些忽略 SIGTERM 信号的进程,则需要编号为 9 的 SIGKILL 信号强行杀死进程,SIGKILL 信号不能被忽略也不能被捕获和自定义处理。
下面写了一段 C 代码,自定义处理了 SIGQUIT、SIGINT、SIGTERM 信号
signal.c
static void signal_handler(int signal_no) {
if (signal_no == SIGQUIT) {
printf("quit signal receive: %d\n", signal_no);
} else if (signal_no == SIGTERM) {
printf("term signal receive: %d\n", signal_no);
} else if (signal_no == SIGINT) {
printf("interrupt signal receive: %d\n", signal_no);
}
}
int main() {
signal(SIGQUIT, signal_handler);
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
for (int i = 0;; i++) {
printf("%d\n", i);
sleep(3);
}
}
编译运行上面的 signal.c 文件
gcc signal.c -o signal
./signal
这种情况下,在终端中Ctrl+C,kill -3,kill -15都没有办法杀掉这个进程,只能用kill -9
0
^Cinterrupt signal receive: 2 // Ctrl+C
1
2
term signal receive: 15 // kill pid
3
4
5
quit signal receive: 3 // kill -3
6
7
8
[1] 46831 killed ./signal // kill -9 成功杀死进程
JVM 对 SIGQUIT 的默认行为是打印所有运行线程的堆栈信息,在类 Unix 系统中,可以通过使用命令 kill -3 pid 来发送 SIGQUIT 信号。运行上面的 MyTestMain,使用 jps 找到整个 JVM 的进程 id,执行 kill -3 pid,在终端就可以看到打印了所有的线程的调用栈信息:
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.51-b03 mixed mode):
"Service Thread" #8 daemon prio=9 os_prio=31 tid=0x00007fe060821000 nid=0x4403 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
...
"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007fe061008800 nid=0x3403 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"main" #1 prio=5 os_prio=31 tid=0x00007fe060003800 nid=0x1003 waiting on condition [0x000070000d203000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at MyTestMain.main(MyTestMain.java:10)
Unix 域套接字(Unix Domain Socket)
使用 TCP 和 UDP 进行 socket 通信是一种广为人知的 socket 使用方式,除了这种方式还有一种称为 Unix 域套接字的方式,可以实现同一主机上的进程间通信。虽然使用 127.0.0.1 环回地址也可以通过网络实现同一主机的进程间通信,但 Unix 域套接字更可靠、效率更高。Docker 守护进程(Docker daemon)使用了 Unix 域套接字,容器中的进程可以通过它与Docker 守护进程进行通信。MySQL 同样提供了域套接字进行访问的方式。
Unix 域套接字是什么?
Unix 域套接字是一个文件,通过 ls 命令可以看到
ls -l
srwxrwxr-x. 1 ya ya 0 9月 8 00:26 tmp.sock
两个进程通过读写这个文件就实现了进程间的信息传递。文件的拥有者和权限决定了谁可以读写这个套接字。
与普通套接字的区别是什么?
-
Unix 域套接字更加高效,Unix 套接字不用进行协议处理,不需要计算序列号,也不需要发送确认报文,只需要复制数据即可 -
Unix 域套接字是可靠的,不会丢失报文,普通套接字是为不可靠通信设计的 -
Unix 域套接字的代码可以非常简单的修改转为普通套接字
下面是一个简单的 C 实现的域套接字的例子
.
├── client.c
└── server.c
server.c 充当 Unix 域套接字服务器,启动后会在当前目录生成一个名为 tmp.sock 的 Unix 域套接字文件,它读取客户端写入的内容并输出。
server.c
int main() {
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "tmp.sock");
int ret = bind(fd, (struct sockaddr *) &addr, sizeof(addr));
listen(fd, 5)
int accept_fd;
char buf[100];
while (1) {
accept_fd = accept(fd, NULL, NULL)) == -1);
while ((ret = read(accept_fd, buf, sizeof(buf))) > 0) {
// 输出客户端传过来的数据
printf("receive %u bytes: %s\n", ret, buf);
}
}
客户端的代码如下:
client.c
int main() {
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "tmp.sock");
connect(fd, (struct sockaddr *) &addr, sizeof(addr)) == -1
int rc;
char buf[100];
// 读取终端标准输入的内容,写入到 Unix 域套接字文件中
while ((rc = read(STDIN_FILENO, buf, sizeof(buf))) > 0) {
write(fd, buf, rc);
}
}
在命令行中进行编译和执行
gcc server.c -o server
gcc client.c -o client
启动两个终端,一个启动 server 端,一个启动 client 端
./server
./client
可以看到当前目录生成了一个 "tmp.sock" 文件
ls -l
srwxrwxr-x. 1 ya ya 0 9月 8 00:08 tmp.sock
在 client 输入 hello,在 server 的终端就可以看到
./server
receive 6 bytes: hello
JVM Attach 过程分析
执行 MyAttachMain,当指定一个不存在的 JVM 进程时,会出现如下的错误:
java -cp /path/to/your/tools.jar:. MyAttachMain 1234
Exception in thread "main" java.io.IOException: No such process
at sun.tools.attach.LinuxVirtualMachine.sendQuitTo(Native Method)
at sun.tools.attach.LinuxVirtualMachine.<init>(LinuxVirtualMachine.java:91)
at sun.tools.attach.LinuxAttachProvider.attachVirtualMachine(LinuxAttachProvider.java:63)
at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:208)
at MyAttachMain.main(MyAttachMain.java:8)
可以看到 VirtualMachine.attach 最终调用了 sendQuitTo 方法,这是一个 native 的方法,底层就是发送了 SIGQUIT 信号给目标 JVM 进程。
前面信号部分我们介绍过,JVM 对 SIGQUIT 的默认行为是 dump 当前的线程堆栈,那为什么调用 VirtualMachine.attach 没有输出调用栈堆栈呢?
对于 Attach 的发起方,假设目标进程为 12345,这部分的详细的过程如下:
1、Attach 端检查临时文件目录是否有 .java_pid12345 文件
这个文件是一个 UNIX 域套接字文件,由 Attach 成功以后的目标 JVM 进程生成。如果这个文件存在,说明正在 Attach 中,可以用这个 socket 进行下一步的通信。如果这个文件不存在则创建一个 .attach_pid12345 文件,这部分的伪代码如下:
String tmpdir = "/tmp";
File socketFile = new File(tmpdir, ".java_pid" + pid);
if (socketFile.exists()) {
File attachFile = new File(tmpdir, ".attach_pid" + pid);
createAttachFile(attachFile.getPath());
}
2、Attach 端检查如果没有 .java_pid12345 文件,创建完 .attach_pid12345 文件以后发送 SIGQUIT 信号给目标 JVM。然后每隔 200ms 检查一次 socket 文件是否已经生成,5s 以后还没有生成则退出,如果有生成则进行 socket 通信
3、对于目标 JVM 进程而言,它的 Signal Dispatcher 线程收到 SIGQUIT 信号以后,会检查 .attach_pid12345 文件是否存在。
-
目标 JVM 如果发现 .attach_pid12345 不存在,则认为这不是一个 attach 操作,执行默认行为,输出当前所有线程的堆栈 -
目标 JVM 如果发现 .attach_pid12345 存在,则认为这是一个 attach 操作,会启动 Attach Listener 线程,负责处理 Attach 请求,同时创建名为 .java_pid12345 的 socket 文件,监听 socket。 源码中 /hotspot/src/share/vm/runtime/os.cpp
这一部分处理的逻辑如下:
#define SIGBREAK SIGQUIT
static void signal_thread_entry(JavaThread* thread, TRAPS) {
while (true) {
int sig;
{
switch (sig) {
case SIGBREAK: {
// Check if the signal is a trigger to start the Attach Listener - in that
// case don't print stack traces.
if (!DisableAttachMechanism && AttachListener::is_init_trigger()) {
continue;
}
...
// Print stack traces
}
}
AttachListener 的 is_init_trigger 在 .attach_pid12345 文件存在的情况下会新建 .java_pid12345 套接字文件,同时监听此套接字,准备 Attach 端发送数据。
那 Attach 端和目标进程用 socket 传递了什么信息呢?可以通过 strace 的方式看到 Attach 端究竟往 socket 里面写了什么:
sudo strace -f java -cp /usr/local/jdk/lib/tools.jar:. MyAttachMain 12345 2> strace.out
...
5841 [pid 3869] socket(AF_LOCAL, SOCK_STREAM, 0) = 5
5842 [pid 3869] connect(5, {sa_family=AF_LOCAL, sun_path="/tmp/.java_pid12345"}, 110) = 0
5843 [pid 3869] write(5, "1", 1) = 1
5844 [pid 3869] write(5, "\0", 1) = 1
5845 [pid 3869] write(5, "load", 4) = 4
5846 [pid 3869] write(5, "\0", 1) = 1
5847 [pid 3869] write(5, "instrument", 10) = 10
5848 [pid 3869] write(5, "\0", 1) = 1
5849 [pid 3869] write(5, "false", 5) = 5
5850 [pid 3869] write(5, "\0", 1) = 1
5855 [pid 3869] write(5, "/home/ya/agent.jar"..., 18 <unfinished ...>
可以看到往 socket 写入的内容如下:
1
\0
load
\0
instrument
\0
false
\0
/home/ya/agent.jar
\0
数据之间用 \0 字符分隔,第一行的 1 表示协议版本,接下来是发送指令 "load instrument false /home/ya/agent.jar" 给目标 JVM,目标 JVM 收到这些数据以后就可以加载相应的 agent jar 包进行字节码的改写。
如果从 socket 的角度来看,VirtualMachine.attach 方法相当于三次握手建连,VirtualMachine.loadAgent 则是握手成功之后发送数据,VirtualMachine.detach 相当于四次挥手断开连接。
这个过程如下图所示:
小结
本文讲解了 javaagent,一起来回顾一下要点:
-
第一,javaagent 是一个使用 instrumentation 的 API 用来改写类文件的 jar 包,可以看作是 JVM 的一个寄生插件。 -
第二,javaagent 有两个重要的入口类:Premain-Class 和 Agent-Class,分别对应入口函数 premain 和 agentmain,其中 agentmain 可以采用远程 attach API 的方式远程挂载另一个 JVM 进程。
本文由 mdnice 多平台发布
相关文章:
字节码调教的入口 —— JVM 的寄生插件 javaagent 那些事
Java Instrumentation 包 Java Instrumentation 概述 Java Instrumentation 这个技术看起来非常神秘,很少有书会详细介绍。但是有很多工具是基于 Instrumentation 来实现的: APM 产品: pinpoint、skywalking、newrelic、听云的 APM 产品等都基于 Instru…...
Blender卡通着色入门
当想到 Blender 和 3D 设计时,你的想法可能会转向风格化渲染或照片级渲染和 VFX。 但是,你是否知道 Blender 还可以创建可与 2D 动漫风格和漫画书类似的图形? 推荐:用 [NSDT编辑器 快速搭建可编程3D场景 1、什么是卡通着色&#x…...
性能调优篇 一、Jvm监控及诊断工具-命令行篇
目录 一、概述1、简单命令行工具 二、jps:查看正在运行的Java程序1、是什么?2、测试3、基本语法 三、jstat:查看jvm统计信息 一、概述 性能诊断是软件工程师 1、简单命令行工具 二、jps:查看正在运行的Java程序 1、是什么&…...
Docker部署MongoDB 5.0.5
1、查看目录 rootwielun:~# tree mongo mongo ├── conf │ └── mongod.conf ├── data ├── docker-compose.yml └── logrootwielun:~# cd mongo rootwielun:~/mongo# chmod 777 log2、配置docker-compose.yml rootwielun:~/mongo# cat docker-compose.yml ve…...
Day18-2-地狱回调-Promise-async-await技术
文章目录 Promise技术一 回调函数二 异步任务三 回调地狱是什么?四 如何解决回调地狱1 PromisePromise基本用法使用Promise解决地狱回调2 async/awaitPromise技术 一 回调函数 当一个函数作为参数传入另一个函数中,并且它不会立即执行,只有当满足一定条件后该函数才可以执…...
echarts范围限制下性能问题
最近实习遇到一个问题,需要对折线图的数据进行范围限制,比如将超过100的设置为100,低于0的设置为0; 原来的代码是创建一个数组,然后遍历原数组,超过的push100,低于0的push0,在中间的…...
wazuh环境配置以及案例复现
目录 wazuh环境配置wazuh案例复现 wazuh环境配置 一、wazuh配置 1.1进入官网下载OVA启动软件 Virtual Machine (OVA) - Installation alternatives (wazuh.com) 1.2点击启动部署,傻瓜式操作 1.3通过账号:wazuh-user,密码:wazuh进…...
解决el-select回显异常 显示option选项的value 而不是显示label
1、问题 回显的value和选项value类型不同 form中v-model"form.userId"是字符串类型 option中:value“item.userId” 选项id是数字类型 2、办法 :value“item.userId” 改为 :value“item.iduserId‘’”(转换成字符串) <el-form-item l…...
【【STM32-SPI通信协议】】
STM32-SPI通信协议 STM32-SPI通信协议 •SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线 •四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO…...
板卡常用前端 数据表操作
两年前写的,现在看,有点想吐, 数据操作表,调试设备用 采用外挂的方法,以前设备的接口命令,简易,换个UI展示很容易 自己写着玩的,公司部分产品再用,前端展示,不涉密 index.html <!doctype html> <html><head><meta chars…...
基于AVR128单片机世界电子时钟的设计
一、系统方案 上电初始化完成系统初始化,液晶滚动显示北京、莫斯科、东京、伦敦、巴黎、纽约等六个城市的标准时间,显示的内容包括地区名及相应地区的年、月、日、星期、时、分、秒。 使用K1按键控制滚动显示或稳定显示某个地区的时间。 使用K3、K4、K5按…...
Electron学习2 使用Electron-vue和Vuetify UI库
Electron学习2 使用Electron-vue和Vuetify UI库 一、Electron-vue简介二、安装yarn三、创建Electron-vue项目1. 关于 electron-builder2. 安装脚手架3. 运行4. 打包应用程序 四、background.js说明1. 引入模块和依赖:2. 注册协议:3. 创建窗口函数&#x…...
Java“牵手”根据商品分类ID获取速卖通商品分类详情页面数据获取方法,速卖通API实现批量商品数据抓取示例
速卖通商城是一个网上购物平台,售卖各类商品,包括服装、鞋类、家居用品、美妆产品、电子产品等。要获取速卖通商品分类详情和商品列表和商品详情页面数据,您可以通过开放平台的接口或者直接访问速卖通商城的网页来获取商品分类详情信息。以下…...
QT 使用图表
目录 1、概念 1.1 坐标轴-QAbstractAxis 1.2 系列-QAbstractSeries 1.3 图例-Legend 1.4 图表-QChart 1.5 视图-QChartView 2、 QT 折线图 2.1 Qt 折线图介绍 2.2 Qt 折线图实现 Qt 图表是专门用来数据可视化的控件 Qt 图表包含折线、饼图、棒图、散点图、范围图等。…...
SSRF 服务器端请求伪造
文章目录 SSRF(curl)网址访问通过file协议访问本地文件dict协议扫描内网主机开放端口 SSRF(file_get_content)网站访问http协议请求内网资源通过file协议访问本地文件 SSRF(Server-Side Request Forgery:服务器端请求伪造) 其形成的原因大都是由于服务端提供了从其他服务器应用…...
shell 05(shell索引数组变量)
一、数组 shell 支持数组 (Array),数组是若干数据的集合,其中的每一份数据都称为数组的元素. 注意Bash shell 只支持一维数组,不支持多维数组。 在 Shell 中,用括号( )来表示数组,数组元素之间用空格来分隔. 语法为&…...
爬虫异常处理:异常捕获与容错机制设计
作为一名专业的爬虫程序员,每天使用爬虫IP面对各种异常情况是我们每天都会遇到的事情。 在爬取数据的过程中,我们经常会遇到网络错误、页面结构变化、被反爬虫机制拦截等问题。在这篇文章中,我将和大家分享一些关于如何处理爬虫异常情况的经…...
Python自动化小技巧21——实现PDF转word功能(程序制作)
案例背景 为什么这个年代PDF转word,某wps居然还要收费.....很多软件都可以实现这个功能,但是效果都有好有坏,而且有的还付费,很麻烦。 那就用python实现这个功能吧,然后把代码打包为.exe的程序,这样随便在…...
Vue使用Element的表格Table显示树形数据,多选框全选无法选中全部节点
使用Element的组件Table表格,当使用树形数据再配合上多选框,如下: 会出现一种问题,点击左上方全选,只能够选中一级树节点,子节点无法被选中,如图所示: 想要实现点击全选就选中所有的…...
SpringBoot生成和解析二维码完整工具类分享(提供Gitee源码)
前言:在日常的开发工作当中可能需要实现一个二维码小功能,我参考了网上很多关于SpringBoot生成二维码的教程,最终还是自己封装了一套完整生成二维码的工具类,可以支持基础的黑白二维码、带颜色的二维码、带Logo的二维码、带颜色和…...
Redis的基本知识(偏八股)
前言 本文篇概念,着重介绍Redis的执行效率、功能作用、数据类型、 执行效率 江湖上都流传这Redis的执行效率是挺快的,那为什么说它快呢?有以下几个原因: 基于内存单线程模型高效数据结构非阻塞I/O 基于内存: 内存的读写效率是…...
react使用antd的table组件,实现点击弹窗显示对应列的内容
特别提醒:不能在table的columns的render里面设置弹窗组件渲染,因为这会导致弹窗显示的始终是最后一行的内容,因为这样渲染的结果是每一行都会重新渲染一遍这个弹窗并且会给传递一个content的值,渲染到最后一行的时候,就…...
c++代码代码逻辑走查
自助生物采集代码 C部分流程...
CSS scoped 属性的原理
scoped 一、scoped 是什么?二、实现原理 一、scoped 是什么? 在 Vue 组件中,为了使样式私有化(模块化),不对全局造成污染,可以在 style 标签上添加 scoped 属性以表示它的只属于当下的模块&am…...
git 查看某个分支是从哪个分支拉出来的
原文链接:https://blog.csdn.net/allanGold/article/details/102478157 git reflog show 分支名git reflog --datelocal | grep 分支名git reflog --datelocal | grep 分支名 $ git reflog --datelocal | grep release3 5c50761 HEAD{Thu Jun 29 12:53:45 2023}: c…...
vue helloworld.vue 点击按钮弹出 dialog,并给dialog传值
1 DataAnalysisVue.Vue -->应该组件文件名和 name: 的名字一致 <template><div><el-dialog :title"dataAnalysisMsg" :visible.sync"dataAnalysisvalue" :before-close"handleClose"><span>{{ dataAnalysisMsg }}&l…...
html动态爱心代码【三】(附源码)
目录 前言 特效 内容修改 完整代码 总结 前言 七夕马上就要到了,为了帮助大家高效表白,下面再给大家带来了实用的HTML浪漫表白代码(附源码)背景音乐,可用于520,情人节,生日,表白等场景,可直…...
mmseg——报错解决:RuntimeError: CUDA error: an illegal memory access was encountered
可能解决方法汇总 GitHub issue相关汇总RuntimeError: CUDA error while trainingCUDA error: an illegal memory access was encountered记录使用mmseg时在计算交叉熵损失遇到的RuntimeError问题与解决方案...
AWS复制EC2文件到S3,g4dn.2xlarge没有NVIDIA GPU 驱动问题
1、给instances权限 action > Security > modify IAM role 把提前创建好的role给这个instance即可 2、复制到bucket aws s3 cp gogo.tar.gz s3://ee547finalbucket不需要手动安装GPU驱动 如果要自己安装,参考https://docs.aws.amazon.com/AWSEC2/latest/U…...
Go语言GIN框架安装与入门
Go语言GIN框架安装与入门 文章目录 Go语言GIN框架安装与入门1. 创建配置环境2. 配置环境3. 下载最新版本Gin4. 编写第一个接口5. 静态页面和资源文件加载6. 各种传参方式6.1 URL传参6.2 路由形式传参6.3 前端给后端传递JSON格式6.4 表单形式传参 7. 路由和路由组8. 项目代码mai…...
用php做的订票网站/怎样做好销售和客户交流
Android Studio安装插件的方式其实和Eclipse大同小异。废话不多说,直接上图:中间部分:你当前已经安装了的插件 Install JetBrains plugin:安装JetBrains类型的插件 Browse Repository:在线安装 Install plugin from di…...
wordpress编辑器不习惯/5年网站seo优化公司
要求:每月1日0点:在不影响业务的情况下,备份整月的数据,保留6次备份。思路:基于MYSQL事件功能,每月按时完成操作RENAME语句具有原子性,新旧表无缝切换RENAME语句仅修改表定义,大表瞬间完成基于上面三点,实现了表数据按…...
杭州专业网站建设/今日热点新闻排行榜
简介 熊猫眼是一个平时用来学习的练手的项目,做这样一个应用的目的主要有两个: 公司项目因为历史原因还有风险控制方面的问题,新的技术不一定能够应用在现有的版本上。所以手痒了就自己弄个应用写一写,持续的更新增加自己的技能熟…...
咨询手机网站建设平台/seo怎么做排名
一、问题描述 当我们的IDEA升级到2020.2的版本时 会发现创建web、spring等项目时和以前的版本有差别 以前版本 2020.2版本 这时候我们该如何新建web项目呢? 二、问题解决 1.创建一个普通的Java项目 2.右键项目名——> Add Framework Support… 3.勾选We…...
百度小程序中心/关键词优化推广公司排名
大家好,我是汤圆,为什么要写面试题呢(这是直接搬的哈哈 表格的文章) ,太无聊了 就看一遍 然后搬一篇,我也要坚持搬运完100篇! 目录 面试题1:你怎么理解ORM框架,常见的ORM框架都有哪些? 正经回…...
企业网站制作/关键词优化公司靠谱推荐
在安装ROS的时候,安装完后需要测试一下ros,测试成功会显示并控制小乌龟,会用到Qt。具体如下 安装ROS成功后,在终端可以运行一个简单的示例程序. 在Terminal中运行以下命令: $ roscore 新开一个terminal,运行以下命令&…...