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.tpcf.cn/news/726846.html

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

相关文章

Ruby CanCanCan 动态定义方法

灵感来自这里https://github.com/kristianmandrup/cantango/wiki/CanCan-vs-CanTango 如果权限不多,我们可以通过这种方式来定义 class CanCan::Abilitydef initialize user, options = {}if !usercan :read, :allendif useradmin_rules if user.roles.include? :adminedit…

STL中push_back和emplace_back效率的对比

文章目录 过程对比1.通过构造参数向vector中插入对象&#xff08;emplace_back更高效&#xff09;2.通过插入实例对象&#xff08;调用copy函数&#xff09;3.通过插入临时对象&#xff08;调用move函数&#xff09; 效率对比emplace_back 的缺点 我们以STL中的vector容器为例。…

解决 Pandas 导出文件出现 dtype: object 字样

文章目录 1. 问题2. 解决方法 1. 问题 python 用 pandas 输出 excel 文件时&#xff0c;发现有些列的单元格出现 “dtype: object” 的字样&#xff0c;如下图&#xff1a; 这是 pandas 没有处理好导致的 2. 解决方法 结果用 .values 进行输出&#xff0c;这样就转成字符串…

Vue的Diff详解

在 Vue 中&#xff0c;当我们更新数据时&#xff0c;Vue 会自动更新视图&#xff0c;这个过程就是虚拟 DOM 的 diff 算法。虚拟 DOM 是一种以 JavaScript 对象的形式表示 DOM 节点的方式&#xff0c;它可以更快地计算出需要更新的节点&#xff0c;从而提高渲染效率。 接下来&a…

聊天室项目

服务器 #include <myhead.h> #define SER_IP "192.168.122.39" #define SER_PORT 8888 typedef struct Node //链表存储客户端的所有信息 {struct sockaddr_in cin; //存储客户端的网络地址信息struct Node *next; }*List; typedef struct Message//消息结构…

洛谷 P1731 [NOI1999] 生日蛋糕

题目 题目链接 自己没看题解写的&#xff0c;摸石头过河&#xff0c;解释一下 首先&#xff0c;输入输出都是正整数。先搞定输入&#xff0c;再判断条件&#xff0c;如果无解&#xff0c;输出0&#xff0c;否则输出蛋糕外表面面积Q&#xff08;这里用全局变量&#xff0c;开l…

数据库的分类和特点介绍

#基础概念# #入门 数据库的主要分类 关系型数据库&#xff08;RDBMS&#xff09; 数据以表格形式存储&#xff0c;通过预定义的关系模型建立数据间的连接&#xff0c;使用SQL作为查询语言。常见的例子包括MySQL、Oracle、SQL Server、PostgreSQL、IBM DB2等。 非关系型数据库…

SEDEX验厂审核重点

SEDEX验厂简介 在全球化的今天&#xff0c;供应链的透明性和可持续性越来越受到人们的关注。为了确保供应链的合规性和可持续性&#xff0c;许多企业开始采用SEDEX验厂这一方法。SEDEX验厂是一种基于国际劳工组织&#xff08;ILO&#xff09;核心劳工标准的供应链审核体系&…

数据库:2024/3/6

作业1&#xff1a;使用C语言完成数据库的增删改 代码&#xff1a; #include <myhead.h>//定义添加员工信息函数 int Add_worker(sqlite3 *ppDb) {//准备sql语句printf("请输入要添加的员工信息:\n");//从终端获取员工信息char rbuf[128]"";fgets(r…

算法-买卖股票的最佳时机

1、题目来源 121. 买卖股票的最佳时机 - 力扣&#xff08;LeetCode&#xff09; 2、题目描述 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖…

React学习

&#x1f4d1;前言 本文主要是【React】——React基础的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 &#x1f304;每日一句&#x…

C++ 哈希表OJ

目录 1、1. 两数之和 2、面试题 01.02. 判定是否互为字符重排 3、217. 存在重复元素 4、 219. 存在重复元素 II 5、49. 字母异位词分组 频繁查找某一个数的时候可以使用哈希表&#xff0c;哈希表可以使用容器&#xff0c;也可以使用数组模拟&#xff0c;当元素是字符串中的字…

算法打卡day9|栈与队列篇01|Leetcode 232.用栈实现队列、225. 用队列实现栈

栈与队列理论基础 栈&#xff08;Stack&#xff09; 栈是一种后进先出&#xff08;LIFO&#xff09;的数据结构,即最近添加到栈中的元素将是第一个被移除的元素。 栈通常有两个主要的操作&#xff1a;push 用于添加一个元素到栈顶&#xff0c;而 pop 用于移除栈顶的元素。此外…

二维码样式修改如何在线处理?在电脑上改二维码图案的方法

随着网络的不断发展&#xff0c;二维码的应用场景不断增多&#xff0c;很多人都会将内容放到二维码中&#xff0c;通过扫码的方式将储存在云端的数据调取显示。而面对不同的用途时&#xff0c;对二维码的样式也会有单独的要求&#xff0c;比如需要改变颜色、加入文字、logo、尺…

网络调试助手使用MQTT协议与Mosquitto通信(3)

一、连接报文 一开始设备需要连接到mqtt服务器&#xff0c;连接时的数据包内需要携带对应的设备ID&#xff0c;以及用户名和密码。这使用默认的用户名和密码。设备ID每一个设备都需要设置为不同的&#xff0c;两个相同的ID只能允许一台设备在线&#xff0c;另一个相同的ID的设备…

【C++庖丁解牛】模版初阶

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 目录 1. 泛型编程2. 函数模…

凌鲨微应用API大全

linksaas-minapp/api 获取微应用信息 export interface MinAppInfo {userId: string; //用户ID(未登录为空字符串)userDisplayName: string;crossHttp: boolean;//打开redis代理时会设置redisProxyToken?: string;redisProxyAddr?: string;//打开mongo代理时会设置mongoPro…

Dgraph 入门教程二《 快速开始》

1、Clound 云 云地址&#xff1a;Dgraph Cloud 登录Clound 云后&#xff0c;可以用云上的东西操作&#xff0c;可以用谷歌账号或者github账号登录。 启动云 &#xff08;1&#xff09;在云控制台&#xff0c;点击 Launch new backend. &#xff08;2&#xff09;选择计划&…

【PowerMockito:编写单元测试过程中原方法使用@Value注解注入的属性出现空指针】

错误场景 执行到Value的属性时会出现空指针&#xff0c;因为Value的属性为null 解决方法 在测试类调用被测试方法前&#xff0c;提前设置属性值&#xff0c;属性可以先自己定义好 ReflectionTestUtils.setField(endpointConnectionService, "exportUdpList", lis…