通过使用 Byte Buddy,便捷地创建 Java Agent

2023-05-16

Java agent 是在另外一个 Java 应用(“目标”应用)启动之前要执行的 Java 程序,这样 agent 就有机会修改目标应用或者应用所运行的环境。在本文中,我们将会从基础内容开始,逐渐增强其功能,借助字节码操作工具 Byte Buddy,使其成为高级的 agent 实现。

在最基本的用例中,Java agent 会用来设置应用属性或者配置特定的环境状态,agent 能够作为可重用和可插入的组件。如下的样例描述了这样的一个 agent,它设置了一个系统属性,在实际的程序中就可以使用该属性了:

public class Agent {	
  public static void premain(String arg) {	
    System.setProperty("my-property", “foo”);	
  }	
}

如上面的代码所述,Java agent 的定义与其他的 Java 程序类似,只不过它使用premain方法替代 main 方法作为入口点。顾名思义,这个方法能够在目标应用的 main 方法之前执行。相对于其他的 Java 程序,编写 agent 并没有特定的规则。有一个很小的区别在于,Java agent 接受一个可选的参数,而不是包含零个或更多参数的数组。

如果要使用这个 agent,必须要将 agent 类和资源打包到 jar 中,并且在 jar 的 manifest 中要将Agent-Class属性设置为包含premain方法的 agent 类。(agent 必须要打包到 jar 文件中,它不能通过拆解的格式进行指定。)接下来,我们需要启动应用程序,并且在命令行中通过 javaagent 参数来引用 jar 文件的位置:

java -javaagent:myAgent.jar -jar myProgram.jar

我们还可以在位置路径上设置可选的 agent 参数。在下面的命令中会启动一个 Java 程序并且添加给定的 agent,将值 myOptions 作为参数提供给premain方法:

java -javaagent:myAgent.jar=myOptions -jar myProgram.jar

通过重复使用javaagent命令,能够添加多个 agent。

但是,Java agent 的功能并不局限于修改应用程序环境的状态,Java agent 能够访问 Java instrumentation API,这样的话,agent 就能修改目标应用程序的代码。Java 虚拟机中这个鲜为人知的特性提供了一个强大的工具,有助于实现面向切面的编程。

如果要对 Java 程序进行这种修改,我们需要在 agent 的premain方法上添加类型为Instrumentation的第二个参数。Instrumentation 参数可以用来执行一系列的任务,比如确定对象以字节为单位的精确大小以及通过注册ClassFileTransformers实际修改类的实现。ClassFileTransformers注册之后,当类加载器(class loader)加载类的时候都会调用它。当它被调用时,在类文件所代表的类加载之前,类文件 transformer 有机会改变或完全替换这个类文件。按照这种方式,在类使用之前,我们能够增强或修改类的行为,如下面的样例所示:

public class Agent {	
 public static void premain(String argument, Instrumentation inst) {	
   inst.addTransformer(new ClassFileTransformer() {	
     @Override	
     public byte[] transform({1}	
       ClassLoader loader,	
       String className,	
       Class<?> classBeingRedefined, // 如果类之前没有加载的话,值为 null	
       ProtectionDomain protectionDomain,	
       byte[] classFileBuffer) {	
       // 返回改变后的类文件。	
     }	
   });	
 }	
}

通过使用Instrumentation实例注册上述的ClassFileTransformer之后,每个类加载的时候,都会调用这个 transformer。为了实现这一点,transformer 会接受一个二进制和类加载器的引用,分别代表了类文件以及试图加载类的类加载器。

Java agent 也可以在 Java 应用的运行期注册,如果是在这种场景下,instrumentation API 允许重新定义已加载的类,这个特性被称之为“HotSwap”。不过,重新定义类仅限于替换方法体。在重新定义类的时候,不能新增或移除类成员,并且类型和签名也不能进行修改。当类第一次加载的时候,并没有这种限制,如果是在这样的场景下,那classBeingRedefined会被设置为 null。

Java 字节码与类文件格式

类文件代表了 Java 类编译之后的状态。类文件中会包含字节码,这些字节码代表了 Java 源码中最初的程序指令。Java 字节码可以视为 Java 虚拟机的语言。实际上,JVM 并不会将 Java 视为编程语言,它只能处理字节码。因为它采用二进制的表现形式,所以相对于程序的源码,它占用的空间更少。除此之外,将程序以字节码的形式进行表现能够更容易地编译 Java 以外的其他语言,如 Scala 或 Clojure,从而让它们运行在 JVM 上。如果没有字节码作为中间语言的话,那么其他的程序在运行之前,可能还需要将其转换为 Java 源码。

但是,在代码处理的时候,这种抽象却带来了一定的成本。如果要将ClassFileTransformer应用到某个类上,那我们不能将该类按照 Java 源码的形式进行处理,甚至不能假设被转换的代码最初是由 Java 编写而成的。更糟糕的是,探查类成员或注解的反射 API 也是禁止使用的,这是因为类加载之前,我们无法访问这些 API,而在转换进程完成之前,是无法进行加载的。

所幸的是,Java 字节码相对来讲是一个比较简单的抽象形式,它包含了很少量的操作,稍微花点功夫我们就能大致将其掌握起来。Java 虚拟机执行程序的时候,会以基于栈的方式来处理值。字节码指令一般会告知虚拟机,需要从操作数栈(operand stack)上弹出值,执行一些操作,然后再将结果压到栈中。

让我们考虑一个简单的样例:将数字 1 和 2 进行相加操作。JVM 首先会将这两个数字压到栈中,这是通过 iconst_1和iconst_2这两个字节指令实现的。iconst_1是个单字节的便捷运算符(operator),它会将数字 1 压到栈中。与之类似,iconst_2会将数字 2 压到栈中。然后,会执行iadd指令,它会将栈中最新的两个值弹出,将它们求和计算的结果重新压到栈中。在类文件中,每个指令并不是以其易于记忆的名称进行存储的,而是以一个字节的形式进行存储,这个字节能够唯一地标记特定的指令,这也是bytecode这个术语的来历。上文所述的字节码指令及其对操作数栈的影响,通过下面的图片进行了可视化。

640?wx_fmt=jpeg

对于人类用户来讲,会更喜欢源码而不是字节码,不过幸运的是 Java 社区创建了多个库,能够解析类文件并将紧凑的字节码暴露为具有名称的指令流。例如,流行的 ASM 库提供了一个简单的 visitor API,它能够将类文件剖析为成员和方法指令,其操作方式类似于阅读 XML 文件时的 SAX 解析器。如果使用 ASM 的话,那上述样例中的字节码可以按照如下的代码来进行实现(在这里,ASM 方式的指令是visitIns,能够提供修正的方法实现):

MethodVisitor methodVisitor = ...	
methodVisitor.visitIns(Opcodes.ICONST_1);	
methodVisitor.visitIns(Opcodes.ICONST_2);	
methodVisitor.visitIns(Opcodes.IADD);

需要注意的是,字节码规范只不过是一种比喻的说法(metaphor),因为 Java 虚拟机允许将程序转换为优化后的机器码(machine code),只要程序的输出能够保证是正确的即可。因为字节码的简洁性,所以在已有的类中取代和修改指令是很简单直接的。因此,使用 ASM 及其底层的 Java 字节码基础就足以实现类转换的 Java agent,这需要注册一个ClassFileTransformer,它会使用这个库来处理其参数。

克服字节码的不足

对于实际的应用来讲,解析原始的类文件依然意味着有很多的手动工作。Java 程序员通常感兴趣的是类型层级结构中的类。例如,某个 Java agent 可能需要修改所有实现给定接口的类。如果要确定某个类的超类,那只靠解析ClassFileTransformer所给定的类文件就不够了,类文件中只包含了直接超类和接口的名字。为了解析可能的超类型关联关系,程序员依然需要定位这些类型的类文件。

在项目中直接使用 ASM 的另外一个困难在于,团队中需要有开发人员学习 Java 字节码的基础知识。在实践中,这往往会导致很多的开发人员不敢再去修改字节码操作相关的代码。如果这样的话,实现 Java agent 很容易为项目的长期维护带来风险。

为了克服这些问题,我们最好使用较高层级的抽象来实现 Java agent,而不是直接操作 Java 字节码。Byte Buddy 是开源的、基于 Apache 2.0 许可证的库,它致力于解决字节码操作和 instrumentation API 的复杂性。Byte Buddy 所声称的目标是将显式的字节码操作隐藏在一个类型安全的领域特定语言背后。通过使用 Byte Buddy,任何熟悉 Java 编程语言的人都有望非常容易地进行字节码操作。

Byte Buddy 简介

Byte Buddy 的目的并不仅仅是为了生成 Java agent。它提供了一个 API 用于生成任意的 Java 类,基于这个生成类的 API,Byte Buddy 提供了额外的 API 来生成 Java agent。

作为 Byte Buddy 的简介,如下的样例展现了如何生成一个简单的类,这个类是 Object 的子类,并且重写了 toString 方法,用来返回“Hello World!”。与原始的 ASM 类似,“intercept”会告诉 Byte Buddy 为拦截到的指令提供方法实现:

  	
Class<?> dynamicType = new ByteBuddy()	
  .subclass(Object.class)	
  .method(ElementMatchers.named("toString"))	
  .intercept(FixedValue.value("Hello World!"))	
  .make()	
  .load(getClass().getClassLoader(),          	
        ClassLoadingStrategy.Default.WRAPPER)	
  .getLoaded();

从上面的代码中,我们可以看到 Byte Buddy 要实现一个方法分为两步。首先,编程人员需要指定一个ElementMatcher,它负责识别一个或多个需要实现的方法。Byte Buddy 提供了功能丰富的预定义拦截器(interceptor),它们暴露在ElementMatchers类中。在上述的例子中,toString方法完全精确匹配了名称,但是,我们也可以匹配更为复杂的代码结构,如类型或注解。

当 Byte Buddy 生成类的时候,它会分析所生成类型的类层级结构。在上述的例子中,Byte Buddy 能够确定所生成的类要继承其超类 Object 的名为 toString 的方法,指定的匹配器会要求 Byte Buddy 重写该方法,这是通过随后的 Implementation 实例实现的,在我们的样例中,这个实例也就是 FixedValue 

当创建子类的时候,Byte Buddy 始终会拦截(intercept)一个匹配的方法,在生成的类中重写该方法。但是,我们在本文稍后将会看到 Byte Buddy 还能够重新定义已有的类,而不必通过子类的方式来实现。在这种情况下,Byte Buddy 会将已有的代码替换为生成的代码,而将原有的代码复制到另外一个合成的(synthetic)方法中。

在我们上面的代码样例中,匹配的方法进行了重写,在实现里面,返回了固定的值“Hello World!”。intercept方法接受 Implementation 类型的参数,Byte Buddy 自带了多个预先定义的实现,如上文所使用的FixedValue类。但是,如果需要的话,可以使用前文所述的 ASM API 将某个方法实现为自定义的字节码,Byte Buddy 本身也是基于 ASM API 实现的。

定义完类的属性之后,就能通过 make 方法来进行生成。在样例应用中,因为用户没有指定类名,所以生成的类会给定一个任意的名称。最终,生成的类将会使用ClassLoadingStrategy来进行加载。通过使用上述的默认 WRAPPER策略,类将会使用一个新的类加载器进行加载,这个类加载器会使用环境类加载器作为父加载器。

类加载之后,使用 Java 反射 API 就可以访问它了。如果没有指定其他构造器的话,Byte Buddy 将会生成类似于父类的构造器,因此生成的类可以使用默认的构造器。这样,我们就可以检验生成的类重写了 toString方法,如下面的代码所示:

assertThat(dynamicType.newInstance().toString(), 	
           is("Hello World!"));

当然,这个生成的类并没有太大的用处。对于实际的应用来讲,大多数方法的返回值是在运行时计算的,这个计算过程要依赖于方法的参数和对象的状态。

通过委托实现 Instrumentation

要实现某个方法,有一种更为灵活的方式,那就是使用 Byte Buddy 的 MethodDelegation。通过使用方法委托,在生成重写的实现时,我们就有可能调用给定类和实例的其他方法。按照这种方式,我们可以使用如下的委托器(delegator)重新编写上述的样例:

class ToStringInterceptor {	
  static String intercept() {	
    return “Hello World!”;	
  }	
}

借助上面的 POJO 拦截器,我们就可以将之前的 FixedValue 实现替换为 MethodDelegation.to(ToStringInterceptor.class):

Class<?> dynamicType = new ByteBuddy()	
  .subclass(Object.class)	
  .method(ElementMatchers.named("toString"))	
  .intercept(MethodDelegation.to(ToStringInterceptor.class))	
  .make()	
  .load(getClass().getClassLoader(),          	
        ClassLoadingStrategy.Default.WRAPPER)	
  .getLoaded();

使用上述的委托器,Byte Buddy 会在 to 方法所给定的拦截目标中,确定最优的调用方法。就ToStringInterceptor.class来讲,选择过程只是非常简单地解析这个类型的唯一静态方法而已。在本例中,只会考虑一个静态方法,因为委托的目标中指定的是一个类。与之不同的是,我们还可以将其委托给某个类的实例,如果是这样的话,Byte Buddy 将会考虑所有的虚方法(virtual method)。如果类或实例上有多个这样的方法,那么 Byte Buddy 首先会排除掉所有与指定 instrumentation 不兼容的方法。在剩余的方法中,库将会选择最佳的匹配者,通常来讲这会是参数最多的方法。我们还可以显式地指定目标方法,这需要缩小合法方法的范围,将ElementMatcher传递到MethodDelegation中,就会进行方法的过滤。例如,通过添加如下的filter,Byte Buddy 只会将名为“intercept”的方法视为委托目标:

  MethodDelegation.to(ToStringInterceptor.class)	
                .filter(ElementMatchers.named(“intercept”))

执行上面的拦截之后,被拦截到的方法依然会打印出“Hello World!”,但是这次的结果是动态计算的,这样的话,我们就可以在拦截器方法上设置断点,所生成的类每次调用toString时,都会触发拦截器的方法。

当我们为拦截器方法设置参数时,就能释放出MethodDelegation的全部威力。这里的参数通常是带有注解的,用来要求 Byte Buddy 在调用拦截器方法时,注入某个特定的值。例如,通过使用@Origin注解,Byte Buddy 提供了添加 instrument 功能的方法的实例,将其作为 Java 反射 API 中类的实例:

class ContextualToStringInterceptor {	
  static String intercept(@Origin Method m) {	
    return “Hello World from ” + m.getName() + “!”;	
  }	
}

当拦截toString方法时,对 instrument 方法的调用将会返回“Hello world from toString!”。

除了@Origin注解以外,Byte Buddy 提供了一组功能丰富的注解。例如,通过在类型为 Callable的参数上使用@Super注解,Byte Buddy 会创建并注入一个代理实例,它能够调用被 instrument 方法的原始代码。如果对于特定的用户场景,所提供的注解不能满足需求或者不太适合的话,我们甚至能够注册自定义的注解,让这些注解注入用户特定的值。

实现方法级别的安全性

可以看到,我们在运行时可以借助简单的 Java 代码,使用 MethodDelegation 来动态重写某个方法。这只是一个简单的样例,但是这项技术可以用到更加实际的应用之中。在本文剩余的内容中,我们将会开发一个样例,它会使用代码生成技术实现一个注解驱动的库,用来限制方法级别的安全性。在我们的第一个迭代中,这个库会通过生成子类的方式来限制安全性。然后,我们将会采取相同的方式来实现 Java agent,完成相同的功能。

样例库会使用如下的注解,允许用户指定某个方法需要考虑安全因素:

@interface Secured {	
  String user();	
}

例如,假设应用需要使用如下的Service类来执行敏感操作,并且只有用户被认证为管理员才能执行该方法。这是通过为执行这个操作的方法声明 Secured 注解来指定的:

class Service {	
  @Secured(user = “ADMIN”)	
  void doSensitiveAction() {	
    // 运行敏感代码...	
  }	
}

我们当然可以将安全检查直接编写到方法中。在实际中,硬编码横切关注点往往会导致复制 - 粘贴的逻辑,使其难以维护。另外,一旦应用需要涉及额外的需求时,如日志、收集调用指标或结果缓存,直接添加这样的代码扩展性不会很好。通过将这样的功能抽取到 agent 中,方法就能很纯粹地关注其业务逻辑,使得代码库能够更易于阅读、测试和维护。

为了让我们规划的库保持尽可能得简单,按照注解的协议声明,如果当前用户不具备注解的用户属性时,将会抛出IllegalStateException异常。通过使用 Byte Buddy,这种行为可以用一个简单的拦截器来实现,如下面样例中的SecurityInterceptor所示,它会通过其静态的 user 域,跟踪当前用户已经进行了登录:

class SecurityInterceptor {	
 	
  static String user = “ANONYMOUS”	
 	
  static void intercept(@Origin Method method) {	
    if (!method.getAnnotation(Secured.class).user().equals(user)) {	
      throw new IllegalStateException(“Wrong user”);	
    }	
  }	
}

通过上面的代码,我们可以看到,即便给定用户授予了访问权限,拦截器也没有调用原始的方法。为了解决这个问题,Byte Buddy 有很多预定义的方法可以实现功能的链接。借助MethodDelegation类的andThen方法,上述的安全检查可以放到原始方法的调用之前,如下面的代码所示。如果用户没有进行认证的话,安全检查将会抛出异常并阻止后续的执行,因此原始方法将不会执行。

将这些功能集合在一起,我们就能生成Service的一个子类,所有带有注解方法的都能恰当地进行安全保护。因为所生成的类是 Service 的子类,所以它能够替代所有类型为Service的变量,并不需要任何的类型转换,如果没有恰当认证的话,调用doSensitiveAction方法就会抛出异常:

new ByteBuddy()	
  .subclass(Service.class)	
  .method(ElementMatchers.isAnnotatedBy(Secured.class))	
  .intercept(MethodDelegation.to(SecurityInterceptor.class)	
                             .andThen(SuperMethodCall.INSTANCE)))	
  .make()	
  .load(getClass().getClassLoader(),   	
        ClassLoadingStrategy.Default.WRAPPER)	
  .getLoaded()	
  .newInstance()	
  .doSensitiveAction();	

不过坏消息是,因为实现 instrumentation 功能的子类是在运行时创建的,所以除了使用 Java 反射以外,没有其他办法创建这样的实例。因此,所有 instrumentation 类的实例都应该通过一个工厂来创建,这个工厂会封装创建 instrumentation 子类的复杂性。这样造成的结果就是,子类 instrumentation 通常会用于框架之中,这些框架本身就需要通过工厂来创建实例,例如,像依赖管理的框架 Spring 或对象 - 关系映射的框架 Hibernate,而对于其他类型的应用来讲,子类 instrumentation 实现起来通常过于复杂。

实现安全功能的 Java agent

通过使用 Java agent,上述安全框架的一个替代实现将会修改Service类的原始字节码,而不是重写它。这样做的话,我们就没有必要创建托管的实例了,只需简单地调用

  new Service().doSensitiveAction()

即可,如果对应的用户没有进行认证的话,就会抛出异常。为了支持这种方式,Byte Buddy 提供一种称之为rebase 某个类的理念。当 rebase 某个类的时候,不会创建子类,所采用的策略是实现 instrumentation 功能的代码将会合并到被 instrument 的类中,从而改变其行为。在添加 instrumentation 功能之后,在被 instrument 的类中,其所有方法的原始代码均可进行访问,因此像SuperMethodCall这样的 instrumentation,工作方式与创建子类是完全一样的。

创建子类与 rebase 的行为是非常类似的,所以两种操作的 API 执行方式是一致的,都会使用相同的DynamicType.Builder接口来描述某个类型。两种形式的 instrumentation 都可以通过ByteBuddy类来进行访问。为了使 Java agent 的定义更加便利,Byte Buddy 还提供了 AgentBuilder类,它希望能够以一种简洁的方式应对一些通用的用户场景。为了定义 Java agent 实现方法级别的安全性,将如下的类定义为 agent 的入口点就足以完成该功能了:

class SecurityAgent {	
  public static void premain(String arg, Instrumentation inst) {	
    new AgentBuilder.Default()	
    .type(ElementMatchers.any())	
    .transform((builder, type) -> builder	
    .method(ElementMatchers.isAnnotatedBy(Secured.class)	
    .intercept(MethodDelegation.to(SecurityInterceptor.class)	
               .andThen(SuperMethodCall.INSTANCE))))	
    .installOn(inst);	
  }	
}

如果将这个 agent 打包为 jar 文件并在命令行中进行指定,那么所有带有Secured注解的方法将会进行“转换”或重定义,从而实现安全保护。如果不激活这个 Java agent 的话,应用在运行时就不包含额外的安全检查。当然,这意味着如果对带有注解的代码进行单元测试的话,这些方法的调用并不需要特殊的搭建过程来模拟安全上下文。Java 运行时会忽略掉无法在 classpath 中找到的注解类型,因此在运行带有注解的方法时,我们甚至完全可以在应用中移除掉安全库。

另外一项优势在于,Java agent 能够很容易地进行叠加。如果在命令行中指定多个 Java agent 的话,每个 agent 都有机会对类进行修改,其顺序就是在命令行中所指定的顺序。例如,我们可以采取这种方式将安全、日志以及监控框架联合在一起,而不需要在这些应用间增添任何形式的集成层。因此,使用 Java agent 实现横切的关注点提供了一种更为模块化的代码编写方式,而不必

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

通过使用 Byte Buddy,便捷地创建 Java Agent 的相关文章

随机推荐

  • s7epaapidll丢失怎么办_s7epaapidll下载

    s7epaapi dll找不到怎么修复 xff1f 很多用户玩单机游戏或者安装软件的时候就出现过这种问题 xff0c 如果是新手第一时间会认为是软件或游戏出错了 xff0c 其实并不是这样 xff0c 其主要原因就是你电脑的该dll文件没有
  • 01-Elasticsearch安装与配置

    一 Elasticsearch 介绍 Elasticsearch是一个实时分布式搜索和分析引擎 二 运行环境 系统 Centos 7JDK 1 8ES版本 7 5 1 下载地址 https www elastic co cn downloa
  • 01-Liunx_用户操作

    一 创建用户组 创建用户组 root 64 localhost bin groupadd 用户组名称 example groupadd test 删除用户组 root 64 localhost bin groupdel 用户组名称 exam
  • 打工与乘公交

    去一个公司打工就如同上了一辆公交车 在上车之前 xff0c 你应该清楚自己打算去哪里 xff0c 打算在哪里下车 有的公交车很豪华 xff0c 有的很破烂 xff0c 但是这并不是重点 xff0c 所有能开到目的地的车都是好车 上了车之后
  • 01-MyBatis Plus-配置信息

    一 官网 URL https mp baomidou com 二 特性 无侵入 xff1a 只做增强不做改变 xff0c 引入它不会对现有工程产生影响 xff0c 如丝般顺滑损耗小 xff1a 启动即会自动注入基本 CURD xff0c 性
  • 五分钟教你手写HashMap

    原作者 xff1a 老铁123 出处 xff1a https blog csdn net qewgd article details 85927183 本文归作者 老铁123 和博客园共有 xff0c 欢迎转载 xff0c 但未经作者同意必
  • Java实现快速排序算法

    原作者 xff1a 老铁123 出处 xff1a https blog csdn net qewgd article details 85949755 本文归作者 老铁123 和博客园共有 xff0c 欢迎转载 xff0c 但未经作者同意必
  • 手写ArrayBlockingQueue

    个人分类 xff1a 算法 编辑 原作者 xff1a 老铁123 出处 xff1a https blog csdn net qewgd article details 88363745 本文归作者 老铁123 和博客园共有 xff0c 欢迎
  • 手写LinkedBlockingQueue

    原作者 xff1a 老铁123 出处 xff1a https blog csdn net qewgd article details 88364742 本文归作者 老铁123 和博客园共有 xff0c 欢迎转载 xff0c 但未经作者同意必
  • Viewbinding自动生成XML的一个对应绑定类

    当你在项目 Module 的build gradle中的android 中设置 buildFeatures viewBinding true 设置完sync一下 xff0c 然后会在项目中看到对应的XML文件的一个继承了ViewBindin
  • AE制作Json动画教程

    本文将从为什么要做动画 xff0c 到动画实现方式 xff0c 再到用AE 43 Bodymovin制作动画 xff0c 结合实际案例行分享 xff0c 希望给新手带来一些启发 首先我们来聊聊 xff0c 我们为什么要做动效 xff1f 1
  • zabbix proxy 表分区

    zabbix server进行表分区的话 xff0c zabbix的内部管家会失效 xff0c 这个时候 xff0c 如果有proxy的话 xff0c 也要进行表分区 xff0c proxy表分区比较简单 xff0c 也不用每天更换分区 步
  • pycharm中unresolved reference怎么解决(配置问题)

    iunresolved reference怎么解决 解决方法 xff1a xff08 本人使用方法二解决的 xff09 方法1 进入PyCharm gt Settings gt Build Excution Deployment gt Co
  • ModuleNotFoundError: No module named ‘_ssl‘

    如果openssl是自己编译安装的 xff0c 安装python时需要注意以下问题 xff1a 从python官网下载的tar gz包或者tgz解压 xff1a 更改 xff1a Python 3 6 6 Modules Setup dis
  • Can I become a good programmer without math and algorithms knowledge?

    Knowledge of algorithms has very little to do with programming skill As some random dude on the internet once said 34 Wh
  • 线程进阶:生产者消费者模式和线程池

    一 生产者消费者模式 这是一种不属于GOF23的设计者模式 这种模式分为三个对象 xff1a 一个生产者 xff0c 一个消费者 xff0c 一个缓存区 生产者 某些程序 进程 线程负责生产数据就属于生产者 消费者 某些程序 进程 线程负责
  • Java异常

    目录 一 什么是异常 xff1f 二 什么是异常处理 三 Java中如何进行异常处理 1 try catah块捕获异常 xff0c 分为三种情况 2 多重catch块 3 finally块 4 声明异常 throws 5 抛出异常 thro
  • Linux系统安装mysql(rpm版)

    目录 Linux系统安装mysql xff08 rpm版 xff09 1 检测当前系统中是否安装MySQL数据库 2 将mysql安装包上传到Linux并解压 3 按照顺序安装rpm软件包 4 启动mysql 5 设置开机自启 6 查看已启
  • ffmpeg 花屏的问题

    ffmpeg 首先说明 xff0c ffmpeg并非做得毫无破绽 1 网络丢包 udp 改成tcp传输并非一定不会丢包 xff0c 这个一定要清楚 xff0c 除此之外 xff0c 如果使用udp xff0c 一定要把udp的接收缓存加得合
  • 通过使用 Byte Buddy,便捷地创建 Java Agent

    Java agent 是在另外一个 Java 应用 xff08 目标 应用 xff09 启动之前要执行的 Java 程序 xff0c 这样 agent 就有机会修改目标应用或者应用所运行的环境 在本文中 xff0c 我们将会从基础内容开始