第10讲:深入剖析 Agent 插件原理,无侵入性埋点

news2025/1/28 1:12:19

AbstractClassEnhancePluginDefine 核心实现

在开始之前,先简单回顾上一课时中关于 AbstractClassEnhancePluginDefine 的一个核心知识点:AbstractClassEnhancePluginDefine 是所有插件的父类,SkywalkingAgent.Transformer 会通过其 enhanceClass() 方法返回的 ClassMatch 对象,匹配到要增强的目标类。在不同的插件实现类中,enhanceClass() 方法返回的 ClassMatch 对象不同,例如:

  • Dubbo 插件拦截的是 com.alibaba.dubbo.monitor.support.MonitorFilter 这个类;
  • Tomcat 插件拦截的是 org.apache.catalina.core.StandardHostValve 这个类。

后面会详细介绍上述两个插件的具体实现。

完成目标类和插件类的匹配之后,会进入 define() 方法,其核心逻辑如下:

  1. 通过 witnessClass() 方法确定当前插件与当前拦截到的目标类的版本是否匹配。若版本不匹配,则 define() 方法直接结束,当前插件类不会增强该类;若版本匹配,则继续后续逻辑。
  2. 进入 enhance() 方法执行增强逻辑。
  3. 设置插件增强标识。

witnessClass() 方法

很多开源组件和工具类库的功能会不断增加,架构也会随之重构,导致不同版本的兼容性得不到很好的保证。例如,MySQL 常用的版本有 5.6、5.7、8.0 多个版本,在使用 JDBC 连接 MySQL 时使用的 mysql-connector-java.jar 包也分为 5.x、6.x、8.x 等版本,对应的 JDBC 协议的版本也各不相同。

SkyWalking Agent 提供的 MySQL 插件本质上是增强 mysql-connector-java.jar 中的关键方法,例如 ConnectionImpl.getInstance() 方法,但在 mysql-connector-java.jar 的 5.x 版本和 8.x 版本中,ConnectionImpl 的包名不同,如下所示:

这仅仅是一个简单的示例,在有的开源组件或类库中,不同版本中同名类的功能和结构已经发生了翻天覆地的变化。要通过一个 SkyWalking Agent 插件完成对一个开源组件所有版本的增强,是非常难实现的,即使勉强能够实现,该插件的实现也会变的非常臃肿,扩展性也会成问题。

SkyWalking 怎么解决这个问题呢?回到 MySQL 示例,SkyWalking 为每个版本的 mysql-connector-java.jar 提供了不同版本的插件,这些插件的 witnessClass() 方法返回值不同,具体返回的是对应版本 mysql-connector-java.jar 所特有的一个类,如下表所示:

若当前类加载器无法扫描到插件 witnessClass() 方法指定的类,表示当前插件版本不合适,即使拦截到了目标类,也不能进行增强。AbstractClassEnhancePluginDefine.define() 方法中的相关片段如下:

String[] witnessClasses = witnessClasses(); 
if (witnessClasses != null) {
    for (String witnessClass : witnessClasses) {
        // 判断指定类加载器中是否存在witnessClasses()指定的类
        if (!WitnessClassFinder.INSTANCE.exist(witnessClass,
               classLoader)) { 
                return null// 若不存在则表示版本不匹配,直接返回
        }
    }
}

增强 static 静态方法

完成上述插件版本的匹配之后,开始进入 enhance() 方法对目标类进行增强。如下图所示, ClassEnhancePluginDefine 继承了 AbstractClassEnhancePluginDefine 抽象类:

在 ClassEnhancePluginDefine 实现的 enhance() 方法中,会分别完成对 static 静态方法以及实例方法的增强:

protected DynamicType.Builder<?> enhance(...) throws PluginException {
    // 增强static方法
    newClassBuilder = this.enhanceClass(typeDescription, 
            newClassBuilder, classLoader); 
    // 增强构造方法和实例方法
    newClassBuilder = this.enhanceInstance(typeDescription, 
            newClassBuilder, classLoader, context); 
    return newClassBuilder;
}

在增强静态方法时会使用到 StaticMethodsInterceptPoint 这个接口,它描述了当前插件要拦截目标类的哪些 static 静态方法,以及委托给哪个类去增强,其定义如下:

public interface StaticMethodsInterceptPoint {
    // 用于匹配目标静态方法
    ElementMatcher<MethodDescription> getMethodsMatcher(); 
    // 拦截到的静态方法交给哪个Interceptor来增强
    String getMethodsInterceptor();
    // 增强过程中是否需要修改参数
    boolean isOverrideArgs();
}

这里以 mysql-8.x-plugin 插件中的实现为例进行说明,其中ConnectionImplCreateInstrumentation 这个插件类的 enhanceClass() 方法如下:

protected ClassMatch enhanceClass() // 拦截目标类为ConnectionImpl
    return byName("com.mysql.cj.jdbc.ConnectionImpl");
}

其 getStaticMethodsInterceptPoints() 方法返回的下面这个 StaticMethodsInterceptPoint 实现(StaticMethodsInterceptPoint 接口的实现基本都是这种匿名内部类):

new StaticMethodsInterceptPoint[] {
    new StaticMethodsInterceptPoint() {
        @Override
        public ElementMatcher<MethodDescription> getMethodsMatcher() {
            return named("getInstance"); // 增强 getInstance()方法
        }

        @Override
        public String getMethodsInterceptor() { 
            // 委托给 ConnectionCreateInterceptor进行增强
            return “org.apache.skywalking.apm.plugin.jdbc
                       .mysql.v8.ConnectionCreateInterceptor”;
        }

        @Override
        public boolean isOverrideArgs() {
            return false; // 增强过程中无需修改方法参数
        }
    }
}

也就是说,ConnectionImplCreateInstrumentation 这个插件拦截的是  com.mysql.jdbc.ConnectionImpl.getInstance() 这个静态方法。

接下来回到 ClassEnhancePluginDefine.enhanceClass() 方法的具体实现:

private DynamicType.Builder<?> enhanceClass(TypeDescription typeDescription,
    DynamicType.Builder<?> newClassBuilder, ClassLoader classLoader) throws PluginException {
    // 获取当前插件的静态方法拦截点,如果该插件不增强静态方法,则该数组为空
    StaticMethodsInterceptPoint[] staticMethodsInterceptPoints = 
          getStaticMethodsInterceptPoints();
    String enhanceOriginClassName = typeDescription.getTypeName();
    for (StaticMethodsInterceptPoint staticMethodsInterceptPoint :
              staticMethodsInterceptPoints) {
        // 进行具体增强的Interceptor名称
        String interceptor = staticMethodsInterceptPoint
              .getMethodsInterceptor();
        // 在增强过程中,是否要修改参数。
        if (staticMethodsInterceptPoint.isOverrideArgs()) {
            // 前面介绍了 Byte Buddy 用法,这里也是一样的,通过method()方法
            // 指定拦截方法的条件
            newClassBuilder = newClassBuilder.method(isStatic()
              .and(staticMethodsInterceptPoint.getMethodsMatcher())) 
                .intercept( 
                    MethodDelegation.withDefaultConfiguration()
                        .withBinders( // 要用Morph注解,需要先绑定
Morph.Binder.install(OverrideCallable.class)
                        // StaticMethodsInterWithOverrideArgs后面展开说
                        ).to(new StaticMethodsInterWithOverrideArgs(interceptor))
                );
        } else { // 下面是不需要修改参数的增强
            newClassBuilder = newClassBuilder.method(isStatic()
             .and(staticMethodsInterceptPoint.getMethodsMatcher()))
                .intercept(MethodDelegation.withDefaultConfiguration()
                .to(new StaticMethodsInter(interceptor))
            );
        }
    }
    return newClassBuilder;
}

根据前文对 Byte Buddy API 的介绍,通过 method() 方法拦截到静态方法之后,如果需要修改方法参数,则会通过 StaticMethodsInterWithOverrideArgs 对象进行增强,其中的 intercept() 方法是其核心实现:

@RuntimeType
public Object intercept(@Origin Class<?> clazz, 
        @AllArguments Object[] allArguments, @Origin Method method,
           @Morph OverrideCallable zuper) throws Throwable {
    // 加载插件指定的StaticMethodsAroundInterceptor
    StaticMethodsAroundInterceptor interceptor = 
       InterceptorInstanceLoader
        .load(staticMethodsAroundInterceptorClassName,
              clazz.getClassLoader());
    MethodInterceptResult result = new MethodInterceptResult();
    // 调用 interceptor.before()做前置处理
    interceptor.beforeMethod(clazz, method, allArguments, 
        method.getParameterTypes(), result);
    Object ret = null;
    try {
        // 根据before()的处理结果判定是否调用目标方法
        if (!result.isContinue()) { 
            ret = result._ret();
        } else {
            // 注意:这里是需要传参的,这些参数我们是可以在before()方法中改动
            // 的,这就是OverrideArgs的意义
            ret = zuper.call(allArguments); 
        }
    } catch (Throwable t) {
        // 如果出现异常,会先通知interceptor中的
        // handleMethodException()方法进行处理
        interceptor.handleMethodException(clazz, method, allArguments, 
            method.getParameterTypes(), t);
        throw t;
    } finally { // 通过after()方法进行后置处理
        ret = interceptor.afterMethod(clazz, method, allArguments,
              method.getParameterTypes(), ret);
    }
    return ret;
}

如果不需要修改方法参数,则会通过 StaticMethodsInter 对象进行增强,其实现与 StaticMethodsInterWithOverrideArgs 类似,唯一区别在于调用目标方法时无法修改参数。

上面使用的 StaticMethodsAroundInterceptor 是个接口,其中定义了如下三个方法:

  • before():在目标方法之前调用。
  • after():在目标方法之后调用。
  • handleMethodException():在目标方法抛出异常时调用。

通过实现 StaticMethodsAroundInterceptor 接口,各个 Agent 插件就可以在静态方法前后添加自定义的逻辑了。

前面提到的 mysql-8.x-plugin 中的 ConnectionImplCreateInstrumentation 自然也实现了该接口。通过对 StaticMethodsInterWithOverrideArgs 以及 StaticMethodsAroundInterceptor 接口的介绍,我们会发现 Agent 插件对静态方法的增强逻辑与 Spring AOP 中环绕通知的逻辑非常类似。

设计模式 TIP
ClassEnhancePluginDefine 是个典型的模板方法模式的使用场景,其 enhanceClass() 方法只实现了增强静态方法的基本流程,真正的增强逻辑全部通过 getStaticMethodsInterceptPoints() 抽象方法推迟到子类实现。在后面增强对象的构造方法和实例方法时,同样会看到类似的实现。

增强实例对象

分析完增强 static 静态方法的相关逻辑之后,我们继续分析增强一个 Java 实例对象的相关逻辑 —— 入口是 enhanceInstance() 方法。enhanceInstance() 方法将分成三个部分来分析其实现:

  • 实现 EnhancedInstance 接口
  • 增强构造方法
  • 增强实例方法

实现 EnhancedInstance 接口

enhanceInstance() 方法首先会为目标类添加了一个字段,同时会让目标类实现 EnhancedInstance 接口,具体实现如下:

// EnhanceContext记录了整个增强过程中的上下文信息,里面就两个boolean值
if (!context.isObjectExtended()) { 
    newClassBuilder = newClassBuilder
        // 定义一个字段private volatile的字段,该字段为Object类型
        .defineField("_$EnhancedClassField_ws", Object.class, 
              ACC_PRIVATE | ACC_VOLATILE)
        // 实现EnhancedInstance接口的方式是读写新
        // 增的"_$EnhancedClassField_ws"字段
        .implement(EnhancedInstance.class) 
        .intercept(FieldAccessor.ofField(CONTEXT_ATTR_NAME));
    context.extendObjectCompleted(); // 标记一下上线文信息
}

EnhancedInstance 接口中定义了 getSkyWalkingDynamicField() 和setSkyWalkingDynamicField() 两个方法,分别读写新增的 _$EnhancedClassField_ws 字段。以前文 demo-webapp 示例中的 HelloWorldController 这个类为例,在 skywalking-agent/debugging/ 目录下可以看到增强后的类如下:

// 实现了EnhancedInstance接口
public class HelloWorldController implements EnhancedInstance {
    private volatile Object _$EnhancedClassField_ws; // 新增字段
    // 对EnhancedInstance的实现
    public Object getSkyWalkingDynamicField() { 
        return this._$EnhancedClassField_ws;
    }

    public void setSkyWalkingDynamicField(Object var1) {
        this._$EnhancedClassField_ws = var1;
    }
    … … // 省略其他业务逻辑相关的方法
}

增强构造方法

接下来,ehanceInstance() 方法会增强实例对象的构造方法,具体流程与增强 static 静态方法的流程类似,唯一区别是这里使用的是 ConstructorInterceptPoint,相关代码片段如下:

ConstructorInterceptPoint[] constructorInterceptPoints = 
   getConstructorsInterceptPoints();
for (ConstructorInterceptPoint constructorInterceptPoint : 
       constructorInterceptPoints) { 
    newClassBuilder = newClassBuilder.constructor(
      constructorInterceptPoint.getConstructorMatcher())
      // 这里对 SuperMethodCall的使用方式和介绍 Byte Buddy基础时说的一毛一样
      .intercept(SuperMethodCall.INSTANCE
          .andThen(MethodDelegation.withDefaultConfiguration() 
             .to(new ConstructorInter(constructorInterceptPoint
                .getConstructorInterceptor(), classLoader))
        )
    );
}

ConstructorInterceptPoint 中描述了插件要增强的构造方法以及增强的 Interceptor 类,与StaticMethodsInterceptPoint 类似,不再展开介绍。

ConstructorInter 与 StaticMethodsInter 类似(这里没有修改构造方法参数的 OverriderArgs 版本,因为此时的构造方法已经调用完成了),ConstructorInter.intercept() 方法的实现如下:

@RuntimeType
public void intercept(@This Object obj, 
        @AllArguments Object[] allArguments) {
    // 前面已经让该对象实现了EnhancedInstance接口,所以这里的类型转换是安全的
    EnhancedInstance targetObject = (EnhancedInstance)obj;
    interceptor.onConstruct(targetObject, allArguments);
}

这里使用的 InstanceConstructorInterceptor 接口与前文介绍的 StaticMethodsAroundInterceptor 接口作用相同,都是留给各个插件去实现增强逻辑的。InstanceConstructorInterceptor 接口的定义如下:

public interface InstanceConstructorInterceptor {
    void onConstruct(EnhancedInstance objInst, Object[] allArguments);
}

mysql-8.x-plugin 插件对 ConnectionImpl 的增强

到这里你可能感觉实现逻辑有点乱,这里我将以 mysql-8.x-plugin 插件为例,把静态方法增强、构造方法增强等逻辑串起来。

首先来看 mysql-connector-java-8.x.jar 中  com.mysql.cj.jdbc.ConnectionImpl.getInstance() 方法,这是我们创建数据连接的最常用方法,具体实现:

public static JdbcConnection getInstance(HostInfo hostInfo)
       throws SQLException {
    return new ConnectionImpl(hostInfo);  // 创建 ConnectionImpl实例
}

先来看 mysql-8.x-plugin 模块的 skywalking-plugin.def 文件,其中定义了ConnectionInstrumentation 这个插件类,它会被 AgentClassLoader 加载,其 enhanceClass() 方法返回的 Matcher 拦截的目标类是 com.mysql.cj.jdbc.ConnectionImpl。

虽然 ConnectionInstrumentation 并不拦截构造方法(因为它的 getConstructorsInterceptPoints() 方法返回的是空数组),但是依然会修改 ConnectionImpl,为其添加 _$EnhancedClassField_ws 字段并实现 EnhanceInstance接口。

在 skywalking-plugin.def 文件中还定义了 ConnectionImplCreateInstrumentation 这个插件类,正如前面介绍的那样,它会拦截 com.mysql.cj.jdbc.ConnectionImpl 的 getInstance() 方法,并委托给 ConnectionCreateInterceptor 进行增强。ConnectionCreateInterceptor 中的 before() 和 handleMethodException() 方法都是空实现,其 after() 方法会记录新建 Connection 的一些信息,具体实现如下:

public Object afterMethod(Class clazz, Method method, 
      Object[] allArguments, Class<?>[] parameterTypes, Object ret) {
    if (ret instanceof EnhancedInstance) { // ConnectionImpl已经被增强了
        // ConnectionInfo中记录了DB名称、DB类型以及地址等等信息,具体构造过程省
        // 略,它会被记录到前面新增的 _$EnhancedClassField_ws 那个字段中
        ConnectionInfo connectionInfo = ...
        ((EnhancedInstance) ret).setSkyWalkingDynamicField(
            connectionInfo);
    }
    return ret;
}

另外,这里还会看到一个 AbstractMysqlInstrumentation 抽象类,继承关系如下图所示:

AbstractMysqlInstrumentation 实现了 witnessClasses() 方法以及 ClassEnhancePluginDefine 中的三个 get*InterceptPoints() 抽象方法(这三个方法都返回 null),其中 witnessClasses() 方法返回"com.mysql.cj.interceptors.QueryInterceptor"字符串,witnessClasses() 方法作用不再重复。

AbstractMysqlInstrumentation 的子类只需根据需求实现相应的 get*InterceptPoints() 方法即可,无需再提供其他剩余 get*InterceptPoints() 方法的空实现。在其他版本的 MySQL 插件中也有 AbstractMysqlInstrumentation 这个抽象类,功能相同,不再重复。

增强实例方法

最后,我们来看 enhanceInstance() 方法对实例方法的增强,其实和增强静态方法的套路一样,我们直接看代码吧:

InstanceMethodsInterceptPoint[] instanceMethodsInterceptPoints = 
    getInstanceMethodsInterceptPoints();
for (InstanceMethodsInterceptPoint instanceMethodsInterceptPoint : 
           instanceMethodsInterceptPoints) {
    String interceptor = instanceMethodsInterceptPoint
         .getMethodsInterceptor();
    // 目标方法的匹配条件
    ElementMatcher.Junction<MethodDescription> junction =
       not(isStatic()).and(instanceMethodsInterceptPoint
         .getMethodsMatcher()); 
    if (instanceMethodsInterceptPoint instanceof 
            DeclaredInstanceMethodsInterceptPoint) {
        // 目标方法必须定义在目标类中
        junction = junction.and(ElementMatchers.
             <MethodDescription>isDeclaredBy(typeDescription)); 
    }
    if (instanceMethodsInterceptPoint.isOverrideArgs()){ //修改方法参数
        newClassBuilder = newClassBuilder
           .method(junction) // 匹配目标方法
           .intercept(MethodDelegation.withDefaultConfiguration()
            // 使用@Morph注解之前,需要通过Morph.Binder绑定一下
            .withBinders(Morph.Binder.install(OverrideCallable.class))
            .to(new InstMethodsInterWithOverrideArgs(interceptor, 
                classLoader)));
    } else {
        // ...省略不需要重载参数的部分...
    }
}

增强实例方法过程中使用到的类,在增强静态方法中都有对应的类,如下表所示:

这些类的具体功能不再展开介绍了。

最后依然以 mysql-8.x-plugin 插件为例介绍一下它对实例方法的增强过程,其中  ConnectionInstrumentation.getInstanceMethodsInterceptPoints() 方法返回了 5 个 InstanceMethodsInterceptPoint 对象,这里只看其中的第一个对象:它负责拦截  ConnectionImpl 的 prepareStatement() 方法,并委托给 CreatePreparedStatementInterceptor(不修改方法参数),具体实现代码就不展示了。

在 CreatePreparedStatementInterceptor 中,before() 和 handleMethodException() 方法都是空实现,其 after() 方法实现如下:

public Object afterMethod(EnhancedInstance objInst, Method method, 
    Object[] allArguments, Class<?>[] argumentsTypes,
    Object ret) throws Throwable {
    if (ret instanceof EnhancedInstance) { // ConnectionImpl已被增强过
        // 更新_$EnhancedClassField_ws字段,StatementEnhanceInfos中不仅封
        // 装了原有的ConnectionInfo,还包含了具体执行的SQL语句和SQL参数等信息
        ((EnhancedInstance)ret).setSkyWalkingDynamicField(
          new StatementEnhanceInfos(
             (ConnectionInfo)objInst.getSkyWalkingDynamicField(),
                 (String)allArguments[0], "PreparedStatement"));
    }
    return ret;
}

InterceptorInstanceLoader

前面加载 Interceptpr 的 ClassLoader 并没有使用 AgentClassLoader 的默认实例或是Application ClassLoader,而是通过 InterceptorInstanceLoader 完成加载的。 在 InterceptorInstanceLoader 里面会维护一个 ClassLoader Cache,以及一个 Instance Cache,如下所示:

// 记录了 instanceKey与实例之间的映射关系,保证单例
static ConcurrentHashMap<String, Object> INSTANCE_CACHE = 
    new ConcurrentHashMap<String, Object>();

// 记录了 targetClassLoader以及其子 AgentClassLoader的对应关系
static Map<ClassLoader, ClassLoader> EXTEND_PLUGIN_CLASSLOADERS = 
     new HashMap<ClassLoader, ClassLoader>();

在通过 InterceptorInstanceLoader.load() 这个静态方法加载 Interceptor 类时的核心逻辑如下:

public static <T> load(String className, 
         ClassLoader targetClassLoader){
    if (targetClassLoader == null) {
          targetClassLoader = 
               InterceptorInstanceLoader.class.getClassLoader();
    }
    // 通过该 instanceKey保证该 Interceptor在一个 ClassLoader中只创建一次
    String instanceKey = className + "_OF_" + 
        targetClassLoader.getClass().getName() + "@" + 
           Integer.toHexString(targetClassLoader.hashCode());
    Object inst = INSTANCE_CACHE.get(instanceKey);
    if (inst == null) {
        // 查找targetClassLoader对应的子AgentClassLoader
        ClassLoader pluginLoader = 
                EXTEND_PLUGIN_CLASSLOADERS.get(targetClassLoader);
        if (pluginLoader == null) {
            // 为 targetClassLoader创建子AgentClassLoader
            pluginLoader = new AgentClassLoader(targetClassLoader);
            EXTEND_PLUGIN_CLASSLOADERS.put(targetClassLoader, 
              pluginLoader);
        }
        // 通过子AgentClassLoader加载Interceptor类
        inst = Class.forName(className, true, 
               pluginLoader).newInstance();
        if (inst != null) { // 记录Interceptor对象
            INSTANCE_CACHE.put(instanceKey, inst);
        }
    }
    return (T) inst;
}

以 demo-webapp 为例,其类加载器的结构如下图所示:

总结

Agent 插件增强目标类的实现,这是 Agent 最核心功能,其中深入分析了增强静态方法、构造方法、实例方法的原理,以及插件如何让目标实例对象实现 EnhanceInstance 接口,如何为目标实例对象添加新字段等。为了帮助你更好的理解,在分析的过程中还以 mysql-8.x-plugin 插件为例将上述核心逻辑串连起来。


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

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

相关文章

信号完整性:反射

反射是怎么形成的 信号的反射和互连线的阻抗密切相关。反射的最直接原因是互连线的阻抗发生了突然变化&#xff0c;只要互连线的阻抗不连续的点&#xff0c;该处就会发生反射。 信号是以电磁波的形式在走线中传播的&#xff0c;如果从传统的电路理论角度去看&#xff0c;是无…

YOLOv5使用自定义数据集实验

上一篇博文中介绍了YOLOv7训练自定义数据集&#xff0c;在这篇文章中&#xff0c;我们主要记录YOLOv5模型的实验过程&#xff0c;用于对比实验。 YOLOv5与YOLOv7毕竟一母同胞&#xff0c;因此部署起来也是极为类似。 数据集 数据集使用的与YOLOv7的实验数据集一样&#xff0c;…

windows服务器自带IIS搭建网站并发布公网访问

文章目录 1.前言2.Windows网页设置2.1 Windows IIS功能设置2.2 IIS网页访问测试 3. Cpolar内网穿透3.1 下载安装Cpolar3.2 Cpolar云端设置3.3 Cpolar本地设置 4.公网访问测试5.结语 转载自远程源码文章&#xff1a;【IIS搭建网站】本地电脑做服务器搭建web站点并公网访问「内网…

VS2019生成和使用lib、dll文件

叠甲&#xff1a;本文非常简略&#xff0c;方法非常朴素&#xff0c;仅供参考。 目录 lib文件 生成lib文件 使用lib文件 dll文件 生成dll文件 使用dll文件 lib文件 生成lib文件 新建项目libTest。 右键项目→属性→配置属性→常规→配置类型&#xff0c;选择“静态库…

【Java】表白墙终章-飞流直下的“甜言蜜语”-瀑布流式布局

飞流直下三千尺&#xff01; 文章目录 【Java】表白墙终章-飞流直下的“甜言蜜语”-瀑布流式布局1. 效果前后对比2. 瀑布流式布局原理思想3. 约定前后端接口4. 后端代码4.1 修改Love类的定义4.2 修改doPost方法4.3 修改save方法4.4 修改doGet方法4.5 修改load方法 5. 前端瀑布流…

面试总结个人版

一、面试题 java 集合 &#xff0c; spring springmvc springboot springcloud 数据库相关的&#xff0c; redis 相关 &#xff0c;mq 相关 &#xff0c;结合业务的场景题 1、part one 集合 HashMap底层原理 HashMap是基于哈希表的Map接口的非同步实现。元素以键值对的形式存…

asp.net教师调课系统VS开发sqlserver数据库web结构c#编程Microsoft Visual Studio

一、源码特点 asp.net教师调课管理系统 是一套完善的web设计管理系统&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为vs2010&#xff0c;数据库为sqlserver2008&#xff0c;使用c#语言开发 asp.net教师调课系统VS开发sqlser…

deadline用WebService提交Job

官方文档 网站链接 进入rest API&#xff0c;点击jobs&#xff0c;找到submit job 这里可以看到消息体需要用到JobInfo和PluginInfo这两个关键的字典&#xff08;json object&#xff09; 拿到对应的键值对 为了填写url请求的消息体 我们需要拿到必须参数的键值对 点击双击…

如何延长电脑硬盘的使用寿命?

在日常使用电脑过程中&#xff0c;一定要做好硬盘的保养和维护&#xff0c;一旦硬盘损坏&#xff0c;保存在硬盘上的数据就会丢失&#xff0c;而且找回数据也是一件很费功夫的事情&#xff0c;甚至有可能永远也找不回来。所以日常工作中定期对资料进行备份&#xff0c;做好电脑…

【算法与数据结构】24、LeetCode两两交换链表中的节点

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;题目要求两两交换节点。在链表当中非常重要就是下一个节点&#xff0c;一旦丢失&#xff0c;这个节点后…

IDL基础语法

1 创建变量 命名规则&#xff1a;变量名必须以字母开头。它们可以包括其它字母&#xff0c;数字&#xff0c;下划线&#xff0c;美元符号。 以下是创建不同数据类型的方法&#xff0c;我们只需了解即可&#xff0c;知道如何创建整型【16位有符号长整型】和浮点型 PRO learn;创…

RedisGraph的整体架构

The architecture of RedisGraph 本文关注RedisGraph的整体架构&#xff0c;分别从图存储模型、索引、并发控制、和执行计划四个方面简要阐述。下图为RedisGraph的整体架构图。 1 图存储模型 了解一个图数据库的架构&#xff0c;最重要的就是其图存储模型&#xff0c;即其中的…

freeswitch 使用 silero-vad 静音拆分使用 fastasr 识别

silero-vad 在git 的评分挺高的测试好像比webrtc vad好下面测试下 silero-vad 支持c 和py 由于识别c的框架少下面使用py 以下基于python3.8torch1.12.0torchaudio 1.12.0 1.由于fastasr 需要16k 所以 将freeswitch的实时音频mediabug 8k转成16k 用socket传到py 模块代码…

二十三种设计模式(待更)

二十三种设计模式 二十三种设计模式结构型1.适配器 相关资料 二十三种设计模式 资料来源于老师讲解以及大佬的设计模式仓库 zhengqingya 结构型 将对象和类按某种布局组成更大的结构&#xff0c;并同时保持结构的灵活和⾼效。 1.适配器 将一个类的接口转换成客户希望的另外…

【小沐学Python】Python实现在线电子书(MkDocs + readthedocs + github + Markdown)

文章目录 1、简介2、安装3、创建新项目4、添加页面5、编辑导航页6、设置主题7、更改图标图标8、构建网站9、部署9.1 准备github项目9.2 注册登录Read the Docs9.3 导入github项目到 Read the Docs 10、Markdown语法10.1 横线10.2 标题10.3 段落10.4 文字高亮10.5 换行10.6 斜体…

你不可不知的八大全新顶级开源项目

导读九年来&#xff0c;Black Duck开源年度奖一直致力于发现过去一年中出现的最具创新性与影响力的开源项目。尽管开源项目阵营一直在快速变化&#xff0c;但年度新人奖一直在为行业趋势提供重要参考。下面&#xff0c;我们将了解这一年中的各位获奖新人! 九年来&#xff0c;B…

adb详细教程(一)-下载安装与环境变量配置

对于Android开发来说&#xff0c;adb是再熟悉不过的调试工具 但其实对于移动端的测试来说&#xff0c;adb也是一个十分重要的、能够提高测试工作效率的工具。 文章目录 一、介绍二、下载地址三、安装四、配置环境变量 一、介绍 全称 adb全称全称为Android Debug Bridge&#x…

【Python】在同一图形中的绘制多个子图

1. 引言 有时我们需要并排绘制两个图形&#xff0c;这不仅是为了更好地利用空间&#xff0c;而且主要是因为为了更加直观地对比分析数据。其实在python中可以利用subplot来实现上述功能。 闲话少说&#xff0c;我们直接开始吧&#xff01; 2. 准备工作 这里&#xff0c;我们…

JavaScript 教程---互联网文档计划

学习目标&#xff1a; 每天记录一章笔记 学习内容&#xff1a; JavaScript 教程---互联网文档计划 笔记时间&#xff1a; 2023-6-5 --- 2023-6-11 学习产出&#xff1a; 1.入门篇 1、JavaScript 的核心语法包含部分 基本语法标准库宿主API 基本语法&#xff1a;比如操作符…

代码随想录第57天

1.回文子串 暴力解法 两层for循环&#xff0c;遍历区间起始位置和终止位置&#xff0c;然后还需要一层遍历判断这个区间是不是回文 动态规划 动规五部曲&#xff1a; 确定dp数组&#xff08;dp table&#xff09;以及下标的含义 如果大家做了很多这种子序列相关的题目&…