SpringCloud(20)之Skywalking Agent原理剖析

一、Agent原理剖析

        使用Skywalking的时候,并没有修改程序中任何一行 Java 代码,这里便使用到了 Java Agent 技术,我 们接下来展开对Java Agent 技术的学习。

1.1 Java Agent

        Java Agent 是从 JDK1.5 开始引入的,算是一个比较老的技术了。作为 Java 的开发工程师,我们常用的  命令之一就是 java 命令,而 Java Agent 本身就是 java 命令的一个参数(即-javaagent)。正如上一课 时接入 SkyWalking Agent 那样, -javaagent 参数之后需要指定一个 jar 包,这个 jar 包需要同时满足下 面两个条件:

  1. 在META-INF目录下的MANIFEST.MF文件中必须指定premain-class配置项;
  2. premain-class配置项指定的类必须提供premain()方法。

         Java 虚拟机启动时,执行 main() 函数之前,虚拟机会先找到-javaagent 命令指定 jar 包,然后执行 premain-class 中的 premain() 方法。用一句概括其功能的话就是:main() 函数之前的一个拦截器。

        使用Java Agent的步骤大概如下:

  1. 定义一个MANIFEST.MF文件,在其中添加premain-class配置项;
  2. 创建premain-class配置项指定的类,并在其中实现premain()方法,方法如下:
  3. 将MANIFEST.MF文件和premain-class指定的类一起打包成一个jar包;
  4. 使用-javaagent指定该jar包的路径可执行其中的premain()方法;
/**** 执行方法拦截* @param agentArgs:-javaagent 命令携带的参数。在前面介绍 SkyWalking Agent 接入时提到*                 agent.service_name 这个配置项的默认值有三种覆盖方式,*                 其中,使用探针配置进行覆盖,探针配置的值就是通过该参数传入的。* @param inst:java.lang.instrumen.Instrumentation 是 Instrumention 包中定义的一个接口,它提供了操作类定义的相关方法。*/public static void premain(String agentArgs, Instrumentation inst){System.out.println("参数:" + agentArgs);}

 1.2定义自己的Agent

 1)探针工程

        创建工程 hailtaxi-agent用来编写agent包,该类需要用 maven-assembly-plugin打包,我们先引入 该插件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.itheima</groupId><artifactId>hailtaxi-agent</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy</artifactId><version>1.9.2</version></dependency><dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy-agent</artifactId><version>1.9.2</version></dependency></dependencies><build><plugins><plugin><artifactId>maven-assembly-plugin</artifactId><configuration><appendAssemblyId>false</appendAssemblyId><descriptorRefs><descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs><archive> <!--自动添加META-INF/MANIFEST.MF --><manifest><!-- 添加 mplementation-*和Specification-*配置项--><addDefaultImplementationEntries>true</addDefaultImplementationEntries><addDefaultSpecificationEntries>true</addDefaultSpecificationEntries></manifest><!-- 将 premain-class 配置项设置为com.jokermqc.LoginAgent--><manifestEntries><Premain-Class>com.jokermqc.LoginAgent</Premain-Class><!--<Premain-Class>com.jokermqc.AgentByteBuddy</Premain-Class>--></manifestEntries></archive></configuration><executions><execution><id>make-assembly</id><phase>package</phase><goals><goal>single</goal></goals></execution></executions></plugin></plugins></build>
</project>

         在该工程中编写一个类 com.itheima.LoginAgent 

public class LoginAgent {/**** 执行方法拦截* @param agentArgs:-javaagent 命令携带的参数。在前面介绍 SkyWalking Agent 接入时提到*                 agent.service_name 这个配置项的默认值有三种覆盖方式,*                 其中,使用探针配置进行覆盖,探针配置的值就是通过该参数传入的。* @param inst:java.lang.instrumen.Instrumentation 是 Instrumention 包中定义的一个接口,它提供了操作类定义的相关方法。*/public static void premain(String agentArgs, Instrumentation inst){System.out.println("参数:" + agentArgs);}
}

        从字面上理解,就是运行在main()函数之前的类。在Java虚拟机启动时,在执行main()函数之前,会先运行指定类的premain()方法,在premain()方法中对class文件进行修改,它有两个入参:

  1. agentArgs:启动参数,在JVM启动时指定;

  2. instrumentation:上文所将的Instrumentation的实例,我们可以在方法中调用上文所讲的方法,注册对应的Class转换器,对Class文件进行修改

        如下图,借助Instrumentation,JVM启动时的处理流程是这样的:JVM会执行指定类的premain()方法,在premain()中可以调用Instrumentation对象的addTransformer方法注册ClassFileTransformer。当JVM加载类时会将类文件的字节数组传递给ClassFileTransformer的transform方法,在transform方法中对Class文件进行解析和修改,之后JVM就会加载转换后的Class文件:

         然后把工程进行打包,得到hailtaxi-agent-1.0-SNAPASHOT.jar,这个就是对应的探针包。

        此时我们把jar包解压,  MANIFEST.MF 内容如下:

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: admin
Build-Jdk: 1.8.0_91
Specification-Title: hailtaxi-agent
Specification-Version: 1.0-SNAPSHOT
Implementation-Title: hailtaxi-agent
Implementation-Version: 1.0-SNAPSHOT
Implementation-Vendor-Id: com.itheima
Premain-Class: com.itheima.LoginAgent

      2)普通工程   

        我们在创建一个普通工程 hailtaxi-user ,在该工程中创建一个普通类并编写main方法:

public class UserInfo {public static void main(String[] args) throws InterruptedException {System.out.println("张三是个中国人!");//调用say()方法say();TimeUnit.SECONDS.sleep(2);}
}

        然后我们再在启动命令中加上刚刚生成的agent包,启动命令如下:

-javaagent:D:/project/skywalking/hailtaxi-agent/target/hailtaxi-agent-1.0- SNAPSHOT.jar=hailtaxi-user

        此时运行效果如下:

1.3 自定义方法统计方法耗时 

        Java Agent 能做的事情非常多,而刚才打印一句日志只是一个能功能展示。要想使用 java agent做 更多事,这里需要关注一下 premain() 方法中的第二个参数: Instrumentation 。Instrumentation  位于 java.lang.instrument 包中,通过这个工具包,我们可以编写一个强大的 Java Agent 程序。

         因为Instrumentation操作字节码非常麻烦,所以一般不会通过Instrumentation 来操作字节码,而是通过Byte Buddy,下面来介绍一下byte Buddy。

1.3.1Byte Buddy介绍

        Byte Buddy 是一个开源 Java 库,其主要功能是帮助用户屏蔽字节码操作,以及复杂的

Instrumentation API  Byte Buddy 提供了一套类型安全的 API 和注解,我们可以直接使用这些 API  注解轻松实现复杂的字节码操作。另外,Byte Buddy 提供了针对 Java Agent 的额API,帮助开发人   员在 Java Agent 场景轻松增强已有代码。

        引入Byte Buddy依赖:

        <dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy</artifactId><version>1.9.2</version></dependency><dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy-agent</artifactId><version>1.9.2</version></dependency>

        创建统计拦截器:

/*** @author maoqichaun* @date 2024年03月05日 18:47*/
public class MethodTimeInterceptor {/*** 这里有一点类似于Spring AOP** @param method   拦截的方法* @param callable 调用对象的代理对象* @return java.lang.Object* @author maoqichuan* @date 2024/3/5*/@RuntimeTypepublic static Object interceptor(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {//时间统计开始long start = System.currentTimeMillis();// 执行原函数Object result = callable.call();//执行时间统计System.out.println(method.getName() + ":" + (System.currentTimeMillis() - start) + "ms");return result;}
}

         这里整体实现类似动态代理执行过程,也类似SpringAop中的环绕通知,其中几个注解我们一起来学习 一下:

@RuntimeType 注解:告诉 Byte Buddy 不要进行严格的参数类型检测,在参数匹配失败时,尝试使用 类型转换方式(runtime type casting)进行类型转换,匹配相应方法。

@Origin 注解:注入目标方法对应的 Method 对象。如果拦截的是字段的话,该注解应该标注到 Field 类型参数。

@SuperCall:这个注解比较特殊,我们要在 intercept() 方法中调用目标方法的话,需要通过这种方 式注入,与 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是,这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。另外, @SuperCall 注解还可以修饰 Runnable 类型的参数,只不过目标方法的返回值就拿不到了。

        配置agent拦截:

public class AgentByteBuddy {/**** 执行方法拦截* @param agentArgs:-javaagent 命令携带的参数。在前面介绍 SkyWalking Agent 接入时提到*                 agent.service_name 这个配置项的默认值有三种覆盖方式,*                 其中,使用探针配置进行覆盖,探针配置的值就是通过该参数传入的。* @param inst:java.lang.instrumen.Instrumentation 是 Instrumention 包中定义的一个接口,它提供了操作类定义的相关方法。*/public static void premain(String agentArgs, Instrumentation inst) throws IllegalAccessException, InstantiationException {//动态构建操作,根据transformer规则执行拦截操作AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {@Overridepublic DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,TypeDescription typeDescription,ClassLoader classLoader,JavaModule javaModule) {//构建拦截规则return builder//method()指定哪些方法需要被拦截,ElementMatchers.any()表示拦截所有方法.method(ElementMatchers.<MethodDescription>any())//intercept()指定拦截上述方法的拦截器.intercept(MethodDelegation.to(MethodTimeInterceptor.class));}};//采用Byte Buddy的AgentBuilder结合Java Agent处理程序new AgentBuilder//采用ByteBuddy作为默认的Agent实例.Default()//拦截匹配方式:类以com.itheima开始(其实即使com.jokermqc包下的所有类).type(ElementMatchers.nameStartsWith("com.jokermqc"))//拦截到的类由transformer处理.transform(transformer)//安装到 Instrumentation.installOn(inst);}
}

         同时将pom.xml中的premain-class替换成 AgentByteBuddy,然后javaagent的jar替换一下即可生效;

二、 Byte Buddy

        在前面学习 Java Agent 技术时,结合 Byte Buddy 技术实现了统计方法执行时间的功能。    Byte Buddy Skywalking中被广泛使用,我们接下来继续学习Byte Buddy,为后续分析 SkyWalking Agent打下基 础。

2.1 Byte Buddy 应用场景

        Java 是一种强类型的编程语言,即要求所有变量和对象都有一个确定的类型,如果在赋值操作中出现类 型不兼容的情况,就会抛出异常。强类型检查在大多数情况下是可行的,然而在某些特殊场景下,强类 型检查则成了巨大的障碍。

        我们在做一些通用工具封装的时候,类型检查就成了很大障碍。比如我们编写一个通用的Dao实现数据 操作,我们根本不知道用户要调用的方法会传几个参数、每个参数是什么类型、需求变更又会出现什么 类型,几乎没法在方法中引用用户方法中定义的任何类型。我们绝大多数通用工具封装都采用了反射机 制,通过反射可以知道用户调用的方法或字段,但是Java反射有很多缺陷:

1:反射性能很差

2:反射能绕开类型安全检查,不安全,比如权限暴力破解

         学完agent后,我们可以基于agent做出一些改变,运行时代码生成在 Java 应用启动之后再动态生成一 些类定义,这样就可以模拟一些只有使用动态编程语言编程才有的特性,同时也不丢失 Java 的强类型  检查。在运行时生成代码需要特别注意的是 Java 类型被 JVM 加载之后,一般不会被垃圾被回收,因此 不应该过度使用代码生成。

        java编程语言代码生成库不止 Byte Buddy一个,以下代码生成库在 Java 中也很流行:

  • Java Proxy:Java Proxy 是 JDK 自带的一个代理工具,它允许为实现了一系列接口的类生成代理类。Java Proxy 要求 目标类必须实现接口是一个非常大限制,例如,在某些场景中,目标类没有实现任何接口且无法修改目 标类的代码实现,Java Proxy 就无法对其进行扩展和增强了。
  • CGLIB:CGLIB 诞生于 Java 初期,但不幸的是没有跟上 Java 平台的发展。虽然 CGLIB 本身是一个相当强大的 库,但也变得越来越复杂。鉴于此,导致许多用户放弃了 CGLIB 
  • Javassist:Javassist 的使用对 Java 开发者来说是非常友好的,它使用Java 源代码字符串和 Javassist 提供的一些简 API  ,共同拼凑出用户想要的 Java 类,Javassist 自带一个编译器,拼凑好的 Java 类在程序运行时会  被编译成为字节码并加载到 JVM 中。Javassist 库简单易用,而且使用 Java 语法构建类与平时写 Java  码类似,但是 Javassist 编译器在性能上比不了 Javac 编译器,而且在动态组合字符串以实现比较复杂

    的逻辑时容易出错。

  • Byte Buddy:Byte Buddy 提供了一种非常灵活且强大的领域特定语言,通过编写简单的 Java 代码即可创建自定义的 运行时类。与此同时,Byte Buddy 还具有非常开放的定制性,能够应付不同复杂度的需求。

        上面所有代码生成技术中,我们推荐使用Byte Buddy,因Byte Buddy代码生成可的性能最高,Byte Buddy 的主要侧重点在于生成更快速的代码,如下图:

2.2 Byte buddy学习 

         我们接下来详细讲解一下Byte Buddy Api ,对重要的方法和类进行深度剖析。

2.2.1 ByteBuddy语法

         任何一个由 Byte Buddy 创建/增强的类型都是通过 ByteBuddy 类的实例来完成的,我们先来学习一下 ByteBuddy类,如下代码:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()// 生成 Object的子类.subclass(Object.class)// 生成类的名称为"com.jokermqc.Type".name("com.jokermqc.Type").make();

         Byte Buddy 动态增强代码总共有三种方式:

subclass:对应 ByteBuddy.subclass() 方法。这种方式比较好理解,就是为目标类(即被增强的类) 生成一个子类,在子类方法中插入动态代码。

rebasing:对应 ByteBuddy.rebasing() 方法。当使用 rebasing 方式增强一个类时,Byte Buddy 保存目标类中所有方法的实现,也就是说,当 Byte Buddy 遇到冲突的字段或方法时,会将原来的字段或 方法实现复制到具有兼容签名的重新命名的私有方法中,而不会抛弃这些字段和方法实现。从而达到不丢失 实现的目的。这些重命名的方法可以继续通过重命名后的名称进行调用。

redefinition:对应 ByteBuddy.redefine() 方法。当重定义一个类时,Byte Buddy 可以对一个已 有的类添加属性和方法,或者删除已经存在的方法实现。如果使用其他的方法实现替换已经存在的方法实现,则原来存在的方法实现就会消失。

         通过上述三种方式完成类的增强之后,我们得到的是 DynamicType.Unloaded 对象,表示的是一个未 加载的类型,我们可以使用 ClassLoadingStrategy 加载此类型。  Byte Buddy 提供了几种类加载策略, 这些策略定义在 ClassLoadingStrategy.Default 中,其中:

  •  WRAPPER 策略:创建一个新的 ClassLoader 来加载动态生成的类型。
  • CHILD_FIRST 策略:创建一个子类优先加载的 ClassLoader ,即打破了双亲委派模型。
  •   INJECTION 策略:使用反射将动态生成的类型直接注入到当前 ClassLoader 中。     

 实现如下:

        Class<?> dynamicClazz = new ByteBuddy()// 生成 Object的子类.subclass(Object.class)// 生成类的名称为"com.joker.Type".name("com.joker.Type").make().load(Demo.class.getClassLoader()//使用WRAPPER 策略加载生成的动态类型.ClassLoadingStrategy.Default.WRAPPER).getLoaded();

        前面动态生成的dynamicType类只是简单的继承了Object类,在实际的应用中动态生成的新类型一般的目的就是为了增强原始的方法,下面通过一个示例展示Byte Buddy如何增强toString()方法;

// 创建ByteBuddy对象String str = new ByteBuddy()// subclass增强方式.subclass(Object.class)// 新类型的类名.name("com.joker.Type")// 拦截其中的toString()方法.method(ElementMatchers.named("toString"))// 让toString()方法返回固定值.intercept(FixedValue.value("Hello World!")).make()// 加载新类型,默认WRAPPER策略.load(ByteBuddy.class.getClassLoader()).getLoaded()// 通过 Java反射创建 com.xxx.Type实例.newInstance()// 调用 toString()方法.toString();

        首先需要关注的是这里的method方法,method()方法可以通过传入的ElementMatchers参数匹配多个需要修改的方法,这的ElementMarchers.named("toString")就是按照方法名来匹配。如果通过存在多个重载方法,也可以使用ElementMarchers其他API来进一步描述方法的签名,如下所示:

// 指定方法名称ElementMatchers.named("toString")// 指定方法的返回值.and(ElementMatchers.returns(String.class))// 指定方法参数.and(ElementMatchers.takesArguments(0));

         接下来要关注的是intercept方法,通过method方法拦截到的所有方法会有intercept()方法指定的Implementtation对象决定如何增强,这里的FixValue.value()会将方法的视线修改为固定值,上面的例子就是固定返回字符串:“Hello World!”。

2.2.2 ByteBuddy创建代理

        我们先创建一个普通类,再为该类创建代理对象,创建代理对方法进行拦截处理。

1)普通类:

public class UserService {//方法1public String username(){System.out.println("com.jokermqc.service.UserService.username.....");return "张三";}//方法2public String address(String username){System.out.println("com.jokermqc.service.UserService.address(String username).....");return username+"来自 【湖北省武汉市】";}//方法3public String address(String username,String city){System.out.println("com.jokermqc.service.UserService.address(String username,String city).....");return username+"来自 【湖北省"+city+"】";}
}

2)创建代理对象

        //创建ByteBuddyUserService userService = new ByteBuddy()//指定创建UserServiceImpl对象的子类.subclass(UserService.class)//匹配方法,所有方法均被拦截.method(ElementMatchers.isDeclaredBy(UserService.class))//任何拦截都返回一个固定值.intercept(MethodDelegation.to(new AspectLog()))//创建动态对象.make().load(ByteBuddy.class.getClassLoader(),ClassLoadingStrategy.Default.INJECTION).getLoaded().newInstance();userService.username();userService.address("王五","武汉");userService.address("张三");

2.2.3 ByteBuddy在程序中的应用

         上面我们创建代理的案例中,把返回值设置成了固定值,但在真实程序汇总通常是要做特定业务流程处 理,比如事务、日志、权限校验等,此时我们需要用到ByteBuddy的MethodDelegation对象,它可以  将拦截的目标方法委托给其他对象处理,这里有几个注解我们先进行说明:

  • @RuntimeType:不进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime typecasting)进行类型转换,匹配相应方法;

  • @This:注入被拦截的目标对象。

  • @AllArguments:注入目标方法的全部参数。

  • Origin:注入目标方法对应的 Method 对象。如果拦截的是字段的话,该注解应该标注到 Field 类型参数。

  • @Super:注入目标对象。通过该对象可以调用目标对象的所有方法。

  • @SuperCall:这个注解比较特殊,我们要在 intercept() 方法中调用目标方法的话,需要通过这种 方式注入,与 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是,   这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。 另外,@SuperCall 注解还可以修饰 Runnable 类型的参数,只不过目标方法的返回值就拿不到了。

        我们对上面的源对象userservice进行一个增强,做一个日志切面;

        1)创建代理对象

    public static void main(String[] args) throws Exception {//创建ByteBuddyUserService userService = new ByteBuddy()//指定创建UserServiceImpl对象的子类.subclass(UserService.class)//匹配方法,所有方法均被拦截.method(ElementMatchers.isDeclaredBy(UserService.class))//任何拦截都返回一个固定值.intercept(MethodDelegation.to(new AspectLog()))//创建动态对象.make().load(ByteBuddy.class.getClassLoader(),ClassLoadingStrategy.Default.INJECTION).getLoaded().newInstance();userService.username();userService.address("王五","武汉");userService.address("张三");}

2)增强处理类

public class AspectLog {@RuntimeTypepublic Object intercept(// 目标对象@This Object obj,// 注入目标方法的全部参数@AllArguments Object[] allArguments,// 调用目标方法,必不可少@SuperCall Callable<?> zuper,// 目标方法@Origin Method method,// 目标对象@Super Object instance) throws Exception {//目标方法执行前执行日志记录System.out.println("准备执行Method="+method.getName());// 调用目标方法Object result = zuper.call();//目标方法执行后执行日志记录System.out.println("方法执行完成Method="+method.getName());return result;}
}

        运行结果如下:

        以上就是Skywalking Agent原理的解析,主要是介绍了什么是Java Agent,以及Byte Buddy的使用,因为在Skywalking中就是使用了ByteBuddy对字节码进行增强,有了这个技术基础才能更好的理解Skywalking源码; 

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入理解 Vuex:从基础到应用场景

前言 在之前的文章中&#xff0c;我们已经对 Vue.js 有了一定的了解。今天我们要对Vue官方的状态共享管理器Vuex进行详细讲解&#xff0c;将其基本吃透&#xff0c;目标是面对大多数业务需求&#xff1b; 一、介绍 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用…

C++——string类

前言&#xff1a;哈喽小伙伴们&#xff0c;从这篇文章开始我们将进行若干个C中的重要的类容器的学习。本篇文章将讲解第一个类容器——string。 目录 一.什么是string类 二.string类常见接口 1.string类对象的常见构造 2.string类对象的容量操作 3. string类对象的访问及遍…

代码随想录第51天|● 300.最长递增子序列 ● 674. 最长连续递增序列 ● 718. 最长重复子数组

文章目录 ● 300.最长递增子序列思路代码&#xff1a; ● 674. 最长连续递增序列思路&#xff1a;代码&#xff1a; ● 718. 最长重复子数组思路&#xff1a;代码一&#xff1a;dp二维数组代码二&#xff1a;滚动数组 ● 300.最长递增子序列 思路 dp[i]表示i之前包括i的以nums…

从 Language Model 到 Chat Application:对话接口的设计与实现

作者&#xff1a;网隐 RTP-LLM 是阿里巴巴大模型预测团队开发的大模型推理加速引擎&#xff0c;作为一个高性能的大模型推理解决方案&#xff0c;它已被广泛应用于阿里内部。本文从对话接口的设计出发&#xff0c;介绍了业界常见方案&#xff0c;并分享了 RTP-LLM 团队在此场景…

MySQL下实现纯SQL语句的递归查询

需求 有一个部门表&#xff0c;部门表中有一个字段用于定义它的父部门&#xff1b; 在实际业务中有一个『部门中心』的业务&#xff1b; 比如采购单&#xff0c;我们需要显示本部门及子部门的采购单显示出来。 结构 数据如下&#xff1a; 实现方式如下&#xff1a; WITH RECUR…

Vue点击切换组件颜色

例如我有一个这样的组件&#xff0c;我希望在点击组件之后由蓝色变成橙色 先把原来的代码附上(简化掉了叉号&#xff09;&#xff1a; <div v-for"(item, index) in words" :key"index" class"scrollbar-demo-item"><span>{{ item …

Unreal 5打开Windows虚拟键盘的权限问题

可以通过以下代码打开Windows虚拟键盘 void UMouseSimulatorBPLibrary::ShowVirtualKeyboard() {TCHAR* OskPath L"C:\\Program Files\\Common Files\\microsoft shared\\ink\\TabTip.exe";if (!FPaths::FileExists(OskPath)){OskPath L"C:\\windows\\system…

比较 2 名无人机驾驶员:借助分析飞得更高

近年来&#xff0c;越来越多的政府和执法机构使用无人机从空中鸟瞰。为了高效执行任务&#xff0c;无人机必须能够快速机动到预定目标。快速机动使它们能够在复杂的环境中航行&#xff0c;并高效地完成任务。成为认证的无人机驾驶员的要求因国家/地区而异&#xff0c;但都要求您…

数字人民币钱包(二)

文章目录 前言一 什么是数字人民币钱包&#xff1f;二 怎么开通数字人民币钱包&#xff1f;三 数字人民币钱包有哪些&#xff1f;四 数字人民币钱包升级 前言 上篇文章梳理了什么是数字人民币&#xff0c;及其特征和相关概念&#xff0c;这篇文章来整理下数字人民币钱包。数字人…

Redis线程模型解析

引言 Redis是一个高性能的键值对&#xff08;key-value&#xff09;内存数据库&#xff0c;以其卓越的读写速度和灵活的数据类型而广受欢迎。在Redis 6.0之前的版本中&#xff0c;它采用的是一种独特的单线程模型来处理客户端的请求。尽管单线程在概念上似乎限制了其扩展性和并…

【笔记】Android ServiceStateTracker 网络状态变化逻辑及SPN更新影响

业务简介 在网络状态变化的时候&#xff08;数据或WiFi&#xff09;&#xff0c;会更新SPN。 基于Android U的代码分析。 分类&#xff1a;SPN Data_Dic-的博客-CSDN博客 功能逻辑 状态说明 飞行模式下注册上WFC的话&#xff0c;注册状态MD上报 regState: NOT_REG_MT_NOT…

【注意】宽泛负载!

放大器输出摆幅会限制可测量的负载电流范围。例如&#xff0c;从 100mV 至 4.9V 的输出摆幅相当于频程约 15 倍的线性输出范围。那么如果要测量 30 倍频程的负载电流&#xff0c;应该怎么做&#xff1f;调节增益&#xff01; 在TIE2E 论坛上为客户提供支持时&#xff0c;我遇到…

CUDA学习笔记05:卷积(sobel)

参考资料 CUDA编程模型系列四(卷积 or sobel边缘检测)_哔哩哔哩_bilibili 强推 ! ! 代码片段 主函数: #include <stdio.h> #include <iostream> #include <math.h> #include <opencv2/opencv.hpp> #include <opencv2/core.hpp> #include &l…

Java设计模式:建造者模式之经典与流式的三种实现(四)

本文将深入探讨Java中建造者模式的两种实现方式&#xff1a;经典建造者与流式建造者。建造者模式是一种创建型设计模式&#xff0c;它允许你构建复杂对象的步骤分解&#xff0c;使得对象的创建过程更加清晰和灵活。我们将通过示例代码详细解释这两种实现方式&#xff0c;并分析…

十:套接字和标准I/O,以及分离I/O流

1 标准I/O函数的优点 C语言标准IO整理 1.1 标准I/O函数的两个优点 标准I/O函数具有良好的移植性。 标准I/O函数可以利用缓冲提高性能 从图中可以看出&#xff0c;使用标准I/O函数传输数据时&#xff0c;经过两个缓冲。例如&#xff0c;使用fputs函数传输字符串 “Hello” 时…

安卓游戏开发之图形渲染技术优劣分析

一、引言 随着移动设备的普及和性能的提升&#xff0c;安卓游戏开发已经成为一个热门领域。在安卓游戏开发中&#xff0c;图形渲染技术是关键的一环。本文将对安卓游戏开发中常用的图形渲染技术进行分析&#xff0c;比较它们的优劣&#xff0c;并探讨它们在不同应用场景下的适用…

关于 selinux 规则

1. 查看selinux状态 SELinux的状态&#xff1a; enforcing&#xff1a;强制&#xff0c;每个受限的进程都必然受限 permissive&#xff1a;允许&#xff0c;每个受限的进程违规操作不会被禁止&#xff0c;但会被记录于审计日志 disabled&#xff1a;禁用 相关命令&#xf…

manjaro 安装 wps 教程

内核: Linux 6.6.16.2 wps-office版本&#xff1a; 11.10.11719-1 本文仅作为参考使用, 如果以上版本差别较大不建议参考 安装wps主体 yay -S wps-office 安装wps字体 &#xff08;如果下载未成功看下面的方法&#xff09; yay -S ttf-waps-fonts 安装wps中文语言 yay …

upload-Labs靶场“11-15”关通关教程

君衍. 一、第十一关 %00截断GET上传1、源码分析2、%00截断GET上传 二、第十二关 %00截断POST上传1、源码分析2、%00截断POST上传 三、第十三关 文件头检测绕过1、源码分析2、文件头检测绕过 四、第十四关 图片检测绕过上传1、源码分析2、图片马绕过上传 五、第十五关 图片检测绕…

mysql学习笔记8——常用5个内置方法

1count 对查询内容进行计数&#xff0c;并返回结果 2as as可以将查询出来结果赋予新名字 3sum sum可以查询某字段特定条件下的和值 4concat concat可以将多列数据合并成一列&#xff0c;只要concat&#xff08;&#xff09;即可 5group_concat group_concat可以把多列…