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
     */
    @RuntimeType
    public 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() {
            @Override
            public 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 {

    //方法1
    public String username(){
        System.out.println("com.jokermqc.service.UserService.username.....");
        return "张三";
    }

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

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

2)创建代理对象

        //创建ByteBuddy
        UserService 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 {
        //创建ByteBuddy
        UserService 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 {

    @RuntimeType
    public 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源码; 

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

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

相关文章

直播预告|小白开箱: 云数据库在五朵云上的评测

3 月 7 日&#xff0c;周四晚上 19:00-20:30 由明说三人行组织&#xff0c;邀请了 NineData 国际总经理(GM) Ni Demai、云猿生数据 CTO &#xff06; 联合创始人子嘉&#xff0c;和《明说三人行》创始人 &主持人明叔&#xff0c;共同围绕《小白开箱: 云数据库在五朵云上的评…

红帆ioffice-udfGetDocStep.asmx存在SQL注入漏洞

产品简介 红帆iOffice.net从最早满足医院行政办公需求&#xff08;传统OA&#xff09;&#xff0c;到目前融合了卫生主管部门的管理规范和众多行业特色应用&#xff0c;是目前唯一定位于解决医院综合业务管理的软件&#xff0c;是最符合医院行业特点的医院综合业务管理平台&am…

Qt插件之输入法插件的构建和使用(一)

文章目录 输入法概述输入法插件实现及调用输入键盘搭建定义样式自定义按钮实现自定义可拖动标签数字符号键盘候选显示控件滑动控件手绘输入控件输入法概述 常见的输入法有三种形式: 1.系统级输入法 2.普通程序输入法 3.程序自带的输入法 系统级输入法就是咱们通常意义上的输入…

SpringBoot3整合Mybatis-plus报错IllegalArgumentException

错误信息 使用的SpringBoot3版本&#xff1a;3.2.3 java.lang.IllegalArgumentException: Invalid value type for attribute factoryBeanObjectType: java.lang.String 第一想法就是感觉是版本太低导致和SpringBoot3不兼容。 查询mybatis-plus最高的版本 <!-- https://m…

“安康杯”安全知识竞赛活动方案

“安康杯”知识竞赛&#xff0c;顾名思义也就是把竞争机制、奖励机制、激励机制应用于安全生产活动中的群众性“安全”与“健康”竞赛。本次竞赛包括 4 个竞赛环节&#xff0c;分别是胜券在握&#xff08;必答题&#xff09;、 刻不容缓&#xff08;抢答题&#xff09;、披荆斩…

volatile关键字的作用 以及 单例模式(饿汉模式与懒汉模式的区别及改进)

文章目录 &#x1f4a1;volatile保证内存可见性&#x1f4a1;单例模式&#x1f4a1;饿汉模式&#x1f4a1;懒汉模式&#x1f4a1;懒汉模式多线程版&#x1f4a1;volatile防止指令重排序 &#x1f4a1;volatile保证内存可见性 Volatile 修饰的变量能够保证“内存可见性”以及防…

数据库(mysql)-新手笔记(主外键,视图)

主外键 主键(唯一性,非空性) 主键是数据库表中的一个或多个字段&#xff0c;其值唯一标识表中的每一行/记录。 唯一性: 主键字段中的每个值都必须是唯一的&#xff0c;不能有两个或更多的记录具有相同的主键值 非空性&#xff1a;主键字段不能包含NULL值。 外键(引用完整 …

AXI4总线解析

一、读地址 AWVALID和AWREADY同时为高时&#xff0c;在这个上升沿&#xff0c;图中黄线&#xff0c;将接下来的数据写入地址40000000中。 在

递增三元组(第九届蓝桥杯)

文章目录 题目原题链接思路分析二分做法1二分做法2双指针做法前缀和解法 题目 原题链接 递增三元组 思路分析 由时间复杂度可知需要至少优化到 O ( n l o g n ) O(nlogn) O(nlogn)才行 而纯暴力枚举三个数组的话&#xff1a; O ( n 3 ) O(n^3) O(n3) 可以考虑将b[]作为标志&…

28000MB 是多少GB 内存?怎么清理内存空间?

在计算机领域&#xff0c;我们经常会听到关于存储容量的单位&#xff0c;如 MB&#xff08;兆字节&#xff09;和 GB&#xff08;千兆字节&#xff09;。如果您在处理计算机内存或存储空间时遇到了 28000MB 这样的容量&#xff0c;您可能会想知道它相当于多少GB。本文将为您解答…

在win10中下载桌面版的docker并在docker中搭建运行基于linux的容器

在win10中下载桌面版的docker 1.背景 在很多时候需要linux系统部署项目&#xff0c;在win10中安装虚拟机并在虚拟机中安装linux系统比较繁琐&#xff0c;可以利用win10自带的hyper-v的虚拟机管理工具&#xff0c;打开该虚拟机管理工具&#xff0c;安装docker&#xff0c;并在…

压测工具jmeter使用

目录 下载 解压 修改配置 启动模拟发送请求 下载 解压 修改配置 启动 下载地址&#xff1a;https://archive.apache.org/dist/jmeter/binaries/ 参考文章&#xff1a;https://www.cnblogs.com/Uni-Hoang/p/15573606.html 一般下zip版本&#xff0c;如apache-jmeter-5.2.1.zip …

ALLegro之单独设置PIN脚与覆铜连接方式

​ 设计PCB时,有很多时候在总电源输入处需要将一部分pin脚设置为全连接,给大电流拓宽通道。然而如果往常针对同一覆铜下的同属性pin脚只能全部设置为全连接或者其他。所以,在初学者手上也出现了“投机份子”,先给全部覆铜改成统一的常规模式,比如十字连接,然后转换成静态…

八、西瓜书——特征选择与稀疏学习

1.子集搜索与评价 对于1个学习任务来说,给定属性集,其中有些属性可能很关键、很有用&#xff0c;另一些属性则可能没什么用&#xff0c;我们将属性称为“特征”(feature),对当前学习任务有用的属性称为“相关特征”(relevant feature)、没什么用的属性称为“无关特征”(irrelev…

基于springboot的车辆充电桩管理系统(系统+数据库+文档)

** &#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;** 一、研究背景…

航天民芯一级代理 MT3608 MT3608L 升压转换器 1.2MHZ

MT3608/MT3608L是恒定频率的6引脚SOT23电流模式升压转换器&#xff0c;适用于小型、低功耗应用。MT3608在1.2MHz&#xff0c;允许使用微小、低成本的频率高度不超过2mm的电容器和电感器。内部软启动可实现较小的浪涌电流和延长电池寿命。MT3608具有自动切换到脉冲的功能轻负载下…

CGAL 5.6.1 - Algebraic Foundations

1. 引言 CGAL 的目标是精确计算非线性对象&#xff0c;特别是定义在代数曲线和曲面上的对象。因此&#xff0c;表示多项式、代数扩展和有限域的类型在相关的实现中扮演着更加重要的角色。为了跟上这些变化&#xff0c;我们引入了这个软件包。由于引入的框架必须特别支持多项式…

安卓玩机工具推荐----高通芯片9008端口读写分区 备份分区 恢复分区 制作线刷包 工具操作解析

上期解析了下adb端口备份分区的有关操作 安卓玩机工具推荐----ADB状态读写分区 备份分区 恢复分区 查看分区号 工具操作解析 在以往的博文中对于高通芯片机型的分区读写已经分享了很多。相关类似博文 安卓备份分区----手动查询安卓系统分区信息 导出系统分区的一些基本操作 …

前端实现一个绕圆心转动的功能

前言&#xff1a; 今天遇到了一个有意思的需求&#xff0c;如何实现一个元素绕某一个点来进行圆周运动&#xff0c;用到了一些初高中的数学知识&#xff0c;实现起来还是挺有趣的&#xff0c;特来分享&#x1f381;。 一. 效果展示 我们先展示效果&#xff0c;如下图所示&…

【NR 定位】3GPP NR Positioning 5G定位标准解读(六)

前言 3GPP NR Positioning 5G定位标准&#xff1a;3GPP TS 38.305 V18 3GPP 标准网址&#xff1a;Directory Listing /ftp/ 【NR 定位】3GPP NR Positioning 5G定位标准解读&#xff08;一&#xff09;-CSDN博客 【NR 定位】3GPP NR Positioning 5G定位标准解读&#xff08;…