lambda表达式底层实现
一、lambda 代码 & 反编译
原始Java代码
假设我们有以下简单的Java程序,它使用Lambda表达式来遍历并打印一个字符串列表:
import java.util.Arrays;
import java.util.List;public class LambdaExample {public static void main(String[] args) {List<String> items = Arrays.asList("Apple", "Banana", "Cherry");items.forEach(item -> System.out.println(item));}
}
public interface Iterable<T> {default void forEach(Consumer<? super T> action) {Objects.requireNonNull(action);for (T t : this) {action.accept(t);}}
}
CFR反编译结果:
/** Decompiled with CFR 0.152.*/
import java.lang.invoke.LambdaMetafactory;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;public class LambdaExample {public static void main(String[] stringArray) {List<String> list = Arrays.asList("Apple", "Banana", "Cherry");list.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)());}private static /* synthetic */ void lambda$main$0(String string) {System.out.println(string);}
}
这是程序的主方法,它创建了一个包含三个字符串的列表,并使用forEach方法遍历这个列表。在原始的Java代码中,这里很可能使用了一个Lambda表达式来打印列表中的每个元素。在反编译的代码中,Lambda表达式被转换成了对LambdaMetafactory.metafactory方法的调用,这个方法在运行时动态生成了一个实现了Consumer接口的类的实例。因为forEach方法入参就是一个函数式接口Consumer<? super T>,即:最终返回Consumer实例对象
二、反编译代码详解
2.1 LambdaMetafactory lambda元工厂类 方法:metafactory
/*** 为了支持Java编程语言中的ambda表达式和方法引用表达式特性,* 本方法提供了一种简便的方式来创建实现一个或多个接口的“函数对象”。这些函数对象是通过委托给一个提供的{@link MethodHandle},* 在适当的类型适配和参数的部分求值之后实现的。通常作为{@code invokedynamic}调用点的<em>引导方法</em>使用。** <p>这是标准的、简化的元工厂方法;通过{@link #altMetafactory(MethodHandles.Lookup, String, MethodType, Object...)}* 提供了额外的灵活性。关于此方法的行为的一般描述,请参见{@link LambdaMetafactory}。** <p>当从此方法返回的{@code CallSite}的目标被调用时,生成的函数对象是实现由{@code invokedType}的返回类型命名的接口的类的实例,* 声明了一个具有由{@code invokedName}和{@code samMethodType}给出的名称和签名的方法。它还可能覆盖来自{@code Object}的额外方法。** @param caller 表示具有调用者访问权限的查找上下文。当与{@code invokedynamic}一起使用时,这由VM自动堆叠。* @param invokedName 要实现的方法的名称。当与{@code invokedynamic}一起使用时,这由{@code InvokeDynamic}结构的{@code NameAndType}提供,并由VM自动堆叠。* @param invokedType {@code CallSite}的预期签名。参数类型代表捕获变量的类型;返回类型是要实现的接口。当与{@code invokedynamic}一起使用时,这由{@code InvokeDynamic}结构的{@code NameAndType}提供,并由VM自动堆叠。如果实现方法是实例方法并且此签名有任何参数,则调用签名中的第一个参数必须对应于接收者。* @param samMethodType 函数对象要实现的方法的签名和返回类型。* @param implMethod 描述应在调用时调用的实现方法的直接方法句柄(适当地适配参数类型、返回类型,并将捕获的参数前置到调用参数中)。* @param instantiatedMethodType 应在调用时动态强制执行的签名和返回类型。这可能与{@code samMethodType}相同,或可能是其特化版本。* @return 一个CallSite,其目标可用于执行捕获,生成由{@code invokedType}命名的接口的实例* @throws LambdaConversionException 如果违反了{@link LambdaMetafactory}中描述的任何链接不变量*/public static CallSite metafactory(MethodHandles.Lookup caller,String invokedName,MethodType invokedType,MethodType samMethodType,MethodHandle implMethod,MethodType instantiatedMethodType)throws LambdaConversionException {// 创建一个内部类Lambda元工厂实例,用于生成和验证lambda表达式的实现AbstractValidatingLambdaMetafactory mf;mf = new InnerClassLambdaMetafactory(caller, invokedType,invokedName, samMethodType,implMethod, instantiatedMethodType,false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);// 验证元工厂方法的参数是否符合要求mf.validateMetafactoryArgs();// 构建并返回一个CallSite,它是lambda表达式或方法引用的动态调用点return mf.buildCallSite();}
java.lang.invoke.LambdaMetafactory#metafactory 详解
metafactory是LambdaMetafactory中的一个静态方法,用于支持lambda表达式和方法引用表达式的动态实现。它是LambdaMetafactory
类的一部分,该类是Java语言中lambda表达式和方法引用的底层支持机制。下面是对这段代码的详细解释:
-
方法的作用和目的:
这个方法的目的是为了动态创建一个实现特定接口的"函数对象"。这个函数对象通过委托给一个提供的MethodHandle
(方法句柄),在适当的类型适配和参数的部分求值后,实现一个或多个接口。这通常用作invokedynamic
调用点的引导方法(bootstrap method),以支持Java编程语言中的lambda表达式和方法引用表达式特性。 -
参数:
- 方法接收六个参数:
caller
、invokedName
、invokedType
、samMethodType
、implMethod
和instantiatedMethodType
。caller
:调用者,这个例子中就是LambdaExample类的MethodHandles.Lookup实例(每个类都可以通过调用MethodHandles.lookup()静态方法来获取一个与该类对应的MethodHandles.Lookup实例。这个Lookup实例代表了调用者的类,并且拥有创建方法句柄(MethodHandle)的权限,这些方法句柄可以访问调用者类中的成员,包括私有成员。),这个实例具有访问LambdaExample类中所有成员的权限。该参数是jvm自动填充。invokedName
:被调用方法的名称,在这个例子中,forEach方法接受一个java.util.function.Consumer类型的参数。Consumer接口定义了一个名为accept的抽象方法。因此,在这个上下文中,invokedName将是accept。 该参数是jvm自动填充。invokedType
:被调用方法的签名类型,这是一个java.lang.invoke.MethodType对象。在lambda表达式或方法引用的上下文中,invokedType描述了期望的调用点的签名,包括参数类型和返回类型。具体来说,invokedType参数定义了:
①调用点期望的参数类型,这些参数类型代表了lambda表达式或方法引用捕获的变量类型(如果有的话)。
②调用点期望的返回类型,这通常是一个函数式接口的类型,lambda表达式或方法引用将会生成一个实现了这个接口的对象。 在这个例子中,forEach方法接受一个java.util.function.Consumer类型的参数。Consumer接口定义了一个接受单个String参数且返回void的accept方法。因此,在这个上下文中,invokedType将是Consumer的方法签名,即接受一个String参数且返回void的方法类型。 该参数是jvm自动填充。samMethodType
:java.lang.invoke.LambdaMetafactory#metafactory方法的参数samMethodType指的是单抽象方法(Single Abstract Method, SAM)的方法类型。这是一个java.lang.invoke.MethodType对象,它描述了目标函数式接口中单个抽象方法的签名,包括参数类型和返回类型。
在使用lambda表达式或方法引用时,通常会有一个函数式接口作为目标类型。函数式接口是指仅定义一个抽象方法的接口。samMethodType参数正是用来描述这个抽象方法的签名。在这个例子中,forEach方法接受一个java.util.function.Consumer类型的参数。Consumer是一个函数式接口,它定义了一个名为accept的抽象方法,该方法接受一个类型为T的参数并返回void。对于这个特定的例子,T是String类型,因此accept方法的签名是(String) -> void。implMethod
:java.lang.invoke.LambdaMetafactory#metafactory方法的参数implMethod指的是实现方法的MethodHandle。这个MethodHandle代表了lambda表达式或方法引用的实际实现体。在lambda表达式或方法引用被转换成动态方法调用时,implMethod就是那个被调用以执行具体操作的方法。
具体来说,implMethod参数描述了:
①方法的实现:这是lambda表达式或方法引用中定义的逻辑的实际代码位置。
②方法的签名:通过MethodHandle的类型,它还隐含地指定了方法的参数类型和返回类型。
在这个例子中,lambda表达式item -> System.out.println(item)对应的implMethod就是System.out.println(String)方法的MethodHandle。这个MethodHandle指向PrintStream类中的println(String)方法,这是因为System.out是一个PrintStream的实例。instantiatedMethodType
:指的是实例化方法的类型。这是一个java.lang.invoke.MethodType对象,它描述了在生成的lambda表达式或方法引用的实例中,目标方法的签名。具体来说,它定义了lambda表达式或方法引用在实现函数式接口时,该接口中抽象方法的调用签名,包括参数类型和返回类型。在这个例子中,forEach方法接受一个java.util.function.Consumer类型的参数。Consumer是一个函数式接口,它定义了一个名为accept的抽象方法,该方法接受一个类型为String的参数并返回void。因此,对于这个特定的例子,instantiatedMethodType将是描述accept方法签名的MethodType对象,即接受一个String参数且返回void的方法类型。
- 方法接收六个参数:
-
逻辑解释:
AbstractValidatingLambdaMetafactory mf;
:声明一个AbstractValidatingLambdaMetafactory
类型的变量mf
,这是一个抽象类,用于验证lambda工厂的参数。mf = new InnerClassLambdaMetafactory(caller, invokedType, invokedName, samMethodType, implMethod, instantiatedMethodType, false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
:实例化mf
为InnerClassLambdaMetafactory
对象,这个对象负责创建实现特定接口的函数对象。传入的参数包括调用者的查找上下文、被调用方法的名称和类型、SAM(Single Abstract Method)接口的方法类型、实现方法的方法句柄、以及实例化方法的类型。false
表示这个lambda对象不需要是可序列化的,EMPTY_CLASS_ARRAY
和EMPTY_MT_ARRAY
分别表示没有额外的接口和方法类型需要被实现或适配。mf.validateMetafactoryArgs();
:调用mf
的validateMetafactoryArgs
方法进行参数验证,确保传入的参数满足lambda表达式和方法引用的链接要求。return mf.buildCallSite();
:调用mf
的buildCallSite
方法构建并返回一个CallSite
对象,这个对象的目标可以用来执行捕获,生成实现了指定接口的实例。
CallSite
是Java中的一个类,它代表了一个动态方法调用点。在Java7的动态语言支持中,CallSite
提供了一种机制,允许方法调用的行为在运行时动态改变,而不是在编译时静态确定。这对于实现动态类型语言或支持某些高级动态特性的静态类型语言(如Java中的lambda表达式和方法引用)非常有用。CallSite
对象包含一个称为目标(target)的MethodHandle
,这个MethodHandle
实际上定义了调用点的行为。当对CallSite
进行方法调用时,实际上是在调用其目标MethodHandle
。
Java中的CallSite
有几种不同的类型,包括:MethodHandleNatives.CallSite
:这是最基本的CallSite
,直接关联一个MethodHandle
作为其调用目标。ConstantCallSite
:一个不可变的CallSite
,其目标在构造时被设置,并且之后不能改变。这对于那些不需要改变的方法调用非常有用,可以提供更好的性能。MutableCallSite
:一个可变的CallSite
,允许改变其目标MethodHandle
。这对于需要根据运行时条件改变调用行为的情况非常有用。VolatileCallSite
:类似于MutableCallSite
,但是对目标MethodHandle
的更新是volatile的,确保了线程安全。
CallSite
和MethodHandle
是Java对动态语言特性的支持的核心部分,它们使得Java能够以更灵活和动态的方式处理方法调用,支持如lambda表达式和方法引用等现代编程特性。metafactory
方法返回的调用点是CallSite
的一个实例。具体来说,根据LambdaMetafactory
的实现,它通常返回的是ConstantCallSite
的一个实例。ConstantCallSite
是CallSite
的一个子类,它表示一个不可变的调用点。一旦ConstantCallSite
的目标方法句柄(MethodHandle)被设置,它就不会改变。这种特性使得ConstantCallSite
非常适合于lambda表达式和方法引用的场景,因为这些场景中的目标方法通常在创建时就已经确定,并且在其生命周期内不需要改变。
·
在LambdaMetafactory
的上下文中,metafactory
方法通过动态生成的类来实现函数接口,并创建一个指向这个实现的方法句柄(MethodHandle)。然后,这个方法句柄被用作ConstantCallSite
的目标,从而创建一个CallSite
实例。这个CallSite
实例在被调用时,会直接调用那个实现了函数接口的动态生成类的方法。
这段代码通过动态创建和配置CallSite
对象,支持了Java中lambda表达式和方法引用表达式的动态实现。
2.2、InnerClassLambdaMetafactory
InnerClassLambdaMetafactory构造函数、buildCallSite构建CallSite调用点
/*** 构造函数:创建一个内部类Lambda元工厂的实例。* 该构造函数用于支持标准情况以及允许序列化或桥接等不常见选项。** @param caller 由VM自动堆叠;代表具有调用者访问权限的查找上下文。* @param invokedType 由VM自动堆叠;被调用方法的签名,包括返回的lambda对象的预期静态类型,* 以及lambda捕获参数的静态类型。如果实现方法是实例方法,调用签名的第一个参数将对应于接收者。* @param samMethodName 转换为lambda或方法引用的函数接口中的方法名称,表示为String。* @param samMethodType 转换为lambda或方法引用的函数接口中的方法类型,表示为MethodType。* @param implMethod 应当被调用的实现方法(适当调整参数类型、返回类型和捕获参数后),当调用结果函数接口实例的方法时。* @param instantiatedMethodType 在从捕获站点实例化类型变量后,主要函数接口方法的签名。* @param isSerializable lambda是否应该是可序列化的?如果设置,目标类型或一个附加的SAM类型必须扩展{@code Serializable}。* @param markerInterfaces lambda对象应该实现的附加接口。* @param additionalBridges 额外的签名,这些签名将被桥接到实现方法。* @throws LambdaConversionException 如果违反了元工厂协议的任何不变量。*/public InnerClassLambdaMetafactory(MethodHandles.Lookup caller,MethodType invokedType,String samMethodName,MethodType samMethodType,MethodHandle implMethod,MethodType instantiatedMethodType,boolean isSerializable,Class<?>[] markerInterfaces,MethodType[] additionalBridges)throws LambdaConversionException {// 调用父类构造函数,初始化基本参数super(caller, invokedType, samMethodName, samMethodType,implMethod, instantiatedMethodType,isSerializable, markerInterfaces, additionalBridges);// 初始化实现方法的类名,将'.'替换为'/'implMethodClassName = implDefiningClass.getName().replace('.', '/');// 初始化实现方法的名称implMethodName = implInfo.getName();// 初始化实现方法的描述符implMethodDesc = implMethodType.toMethodDescriptorString();// 初始化实现方法返回类型的类implMethodReturnClass = (implKind == MethodHandleInfo.REF_newInvokeSpecial)? implDefiningClass: implMethodType.returnType();// 初始化生成类构造函数的类型constructorType = invokedType.changeReturnType(Void.TYPE);// 生成并初始化lambda类的名称lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();// 【重要⭐️⭐️⭐️⭐️⭐️】初始化ASM类写入器cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);// 初始化构造函数参数名称和描述符数组int parameterCount = invokedType.parameterCount();if (parameterCount > 0) {// 初始化参数名和参数描述数组,大小为方法参数的数量argNames = new String[parameterCount];argDescs = new String[parameterCount];// 遍历所有参数,生成参数名和参数描述for (int i = 0; i < parameterCount; i++) {// 为每个参数生成一个唯一的名称,格式为"arg$序号"argNames[i] = "arg$" + (i + 1);// 使用BytecodeDescriptor工具类将参数类型转换为字符串描述形式argDescs[i] = BytecodeDescriptor.unparse(invokedType.parameterType(i));}} else {// 当调用类型参数计数为0时,初始化参数名和参数描述数组为空字符串数组argNames = argDescs = EMPTY_STRING_ARRAY;}}/*** 构建CallSite。生成实现功能接口的类文件,定义类,如果没有参数则创建类的实例,* 该实例将由CallSite返回,否则,生成的句柄将调用类的构造函数。** @return CallSite,调用时,将返回一个功能接口的实例* @throws ReflectiveOperationException 反射操作异常* @throws LambdaConversionException 如果没有找到正确形式的功能接口*/@OverrideCallSite buildCallSite() throws LambdaConversionException {// 生成实现了函数接口的内部类final Class<?> innerClass = spinInnerClass();// 如果调用类型没有参数,即无需捕获的变量if (invokedType.parameterCount() == 0) {// 通过反射获取一个内部类的所有构造函数,并在只有一个构造函数的情况下,将这个唯一的构造函数设置为可访问的。final Constructor<?>[] ctrs = AccessController.doPrivileged(new PrivilegedAction<Constructor<?>[]>() {@Overridepublic Constructor<?>[] run() {// 返回了innerClass(内部类)的所有构造函数,包括私有的。Constructor<?>[] ctrs = innerClass.getDeclaredConstructors();if (ctrs.length == 1) {// 如果只有一个构造函数,设置为可访问ctrs[0].setAccessible(true);}return ctrs;}});// 确保只有一个构造函数if (ctrs.length != 1) {throw new LambdaConversionException("Expected one lambda constructor for "+ innerClass.getCanonicalName() + ", got " + ctrs.length);}try {// 通过构造函数实例化对象Object inst = ctrs[0].newInstance();// 创建并返回一个持有lambda对象的ConstantCallSitereturn new ConstantCallSite(MethodHandles.constant(samBase, inst));}catch (ReflectiveOperationException e) {throw new LambdaConversionException("Exception instantiating lambda object", e);}} else {// 如果有参数,需要通过静态方法来创建CallSitetry {// 确保类已经被完全初始化UNSAFE.ensureClassInitialized(innerClass);// 查找静态方法并创建CallSitereturn new ConstantCallSite(MethodHandles.Lookup.IMPL_LOOKUP.findStatic(innerClass, NAME_FACTORY, invokedType));}catch (ReflectiveOperationException e) {throw new LambdaConversionException("Exception finding constructor", e);}}}/*** 生成并返回一个实现了功能接口的类文件。** @implNote 生成的类文件不包含SAM方法可能存在的异常签名信息,* 旨在减少类文件大小。这是无害的,因为已检查的异常会被擦除,* 没有人会针对这个类文件进行编译,我们不保证lambda对象的反射属性。** @return 实现了功能接口的类* @throws LambdaConversionException 如果没有找到正确形式的功能接口*/private Class<?> spinInnerClass() throws LambdaConversionException {// 构建一个字符串数组 interfaces,该数组包含了要实现的接口的内部名称(即将.替换为/的全限定类名),同时确保没有重复的接口,并检查是否意外地实现了 Serializable 接口。String[] interfaces;// 获取函数式接口的内部名称,将.替换为/。String samIntf = samBase.getName().replace('.', '/');// 检查基础函数式接口是否意外实现了 Serializable 接口。boolean accidentallySerializable = !isSerializable && Serializable.class.isAssignableFrom(samBase);// 如果没有额外的标记接口,直接使用函数式接口的内部名称作为 interfaces 的唯一元素。if (markerInterfaces.length == 0) {interfaces = new String[]{samIntf};} else {// 如果 markerInterfaces 非空,确保没有重复的接口(ClassFormatError),使用 LinkedHashSet 来存储接口名称,确保不会有重复。Set<String> itfs = new LinkedHashSet<>(markerInterfaces.length + 1);// 将函数式接口的内部名称添加到集合中itfs.add(samIntf);// 遍历额外的标记接口,将它们的内部名称添加到集合中,并检查是否意外实现了 Serializable 接口。for (Class<?> markerInterface : markerInterfaces) {itfs.add(markerInterface.getName().replace('.', '/'));accidentallySerializable |= !isSerializable && Serializable.class.isAssignableFrom(markerInterface);}// 将接口名称集合转换为字符串数组。interfaces = itfs.toArray(new String[itfs.size()]);}/**cw 是 ClassWriter 的实例,它是 ASM(一个通用的 Java 字节码操作和分析框架)库中的一个类。ClassWriter 用于动态生成类或接口的二进制字节码。在上下文中,cw 被用来构建和定义一个新的类,这个类是在运行时动态生成的,用于实现特定的功能接口,通常是为了支持 Java 中的 lambda 表达式。
通过调用 ClassWriter 的方法,如 visit、visitMethod 和 visitField,可以分别定义类的基本信息、方法和字段。最终,通过调用 cw.toByteArray() 方法,可以获取到这个动态生成的类的字节码数组,这个数组可以被加载到 JVM 中,从而创建出一个新的类实例。*/// 定义了一个类,这个类是final和synthetic的,继承自Object类,并实现了interfaces数组中指定的接口。lambdaClassName是这个类的名称。// 其中:// ACC_FINAL 表示这个类是final的// ACC_SYNTHETIC 表示这个类是synthetic的,synthetic标记表明这个类是由编译器自动生成的,而非直接来自源代码。// lambdaClassName 是动态生成的类名cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,lambdaClassName, null,JAVA_LANG_OBJECT, interfaces);// 生成构造函数中要填充的最终字段for (int i = 0; i < argDescs.length; i++) {// 生成一个private final字段来存储这些参数的值。FieldVisitor fv = cw.visitField(ACC_PRIVATE + ACC_FINAL,argNames[i],argDescs[i],null, null);/**这行代码的作用是结束一个字段的访问。在ASM中,每当开始定义一个新的字段时,都会通过调用visitField方法返回一个FieldVisitor对象,通过这个对象可以定义字段的属性。当字段的定义结束时,需要调用visitEnd方法来标志这个过程的结束。*/ fv.visitEnd();}// 生成构造函数generateConstructor();// 判断是检查invokedType(lambda表达式的目标类型)是否有参数。if (invokedType.parameterCount() != 0) {// 这个方法的作用是生成工厂方法。工厂方法是一个特殊的方法,用于动态生成并返回实现了函数式接口的类的实例。这个过程通常涉及到字节码的生成和类的加载。generateFactory();}/**这行代码通过调用 ClassWriter 的 visitMethod 方法创建了一个新的方法。这个方法的访问级别是 public,方法名是 samMethodName,这是一个从外部传入的参数,表示要实现的SAM接口中的方法名。samMethodType.toMethodDescriptorString() 将方法的签名转换为字符串形式,用于定义方法的参数类型和返回类型。最后两个 null 参数分别表示这个方法的签名和异常,这里不使用这些高级特性,所以传入 null。*/MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, samMethodName,samMethodType.toMethodDescriptorString(), null, null);// 这行代码给刚才创建的方法添加了一个注解 LambdaForm$Hidden。这个注解是内部使用的,用于标记这个方法不应该被外部调用或者看到。true 参数表示这个注解是在运行时可见的。mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);// 这行代码实际上是生成方法体的关键步骤。它创建了一个 ForwardingMethodGenerator 对象,这个对象负责生成方法体的字节码。generate 方法接受一个 MethodType 对象 samMethodType 作为参数,这个对象描述了SAM接口方法的参数类型和返回类型。generate 方法根据这个信息,动态生成字节码,这些字节码实现了将调用转发到实际的目标方法上。new ForwardingMethodGenerator(mv).generate(samMethodType);/**这段代码的主要作用是为了生成桥接方法(Bridge Methods),这些方法用于处理泛型擦除后的类型不匹配问题。在Java中,泛型信息在编译时会被擦除,而桥接方法则用于在运行时保持类型的正确性。这段代码是在动态生成的类中添加这些桥接方法的过程。*/// additionalBridges 是一个包含了需要生成桥接方法的 MethodType 对象的数组。if (additionalBridges != null) {for (MethodType mt : additionalBridges) {// 为每个桥接方法类型生成方法:通过调用 cw.visitMethod 方法生成桥接方法。这里的 cw 是一个 ClassWriter 对象,用于动态生成类的字节码。ACC_PUBLIC|ACC_BRIDGE 是方法的访问标志,表示这是一个公开的桥接方法。samMethodName 是要实现的函数式接口的方法名,mt.toMethodDescriptorString() 将方法类型转换为方法描述符字符串,用于指定方法的签名。mv = cw.visitMethod(ACC_PUBLIC|ACC_BRIDGE, samMethodName,mt.toMethodDescriptorString(), null, null);// 添加方法注解:通过调用 mv.visitAnnotation 方法为生成的桥接方法添加注解。这里的注解是 "Ljava/lang/invoke/LambdaForm$Hidden;",表示这个方法是由lambda表达式生成的,不应该被直接调用。mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);// 生成方法体:通过创建一个新的 ForwardingMethodGenerator 对象并调用其 generate 方法来生成桥接方法的方法体。这个方法体基本上是将调用转发到实际的实现方法上。new ForwardingMethodGenerator(mv).generate(mt);}}/**这段代码的作用是根据是否需要序列化,生成对应的方法。具体来说,如果需要生成的类是可序列化的,则生成序列化友好的方法;如果不是故意的可序列化(即无意中成为可序列化的),则生成序列化敌对的方法。最后,调用 cw.visitEnd() 来完成类的定义。1.判断是否需要序列化:通过 if (isSerializable) 判断,如果 isSerializable 为 true,则表示需要生成的类是可序列化的,此时会调用 generateSerializationFriendlyMethods() 方法生成序列化友好的方法。2.判断是否无意中成为可序列化:如果 isSerializable 为 false,则进入 else if (accidentallySerializable) 判断,accidentallySerializable 为 true 表示类无意中成为了可序列化的(例如,通过实现了某个可序列化的接口)。此时会调用 generateSerializationHostileMethods() 方法生成序列化敌对的方法,这可能是为了避免序列化带来的潜在问题或性能影响。3.完成类的定义:无论是否需要序列化,最后都会执行 cw.visitEnd(),这是ASM库中的方法,用于完成类的定义。这一步是生成类文件的最后一步,标志着类定义的结束。*/if (isSerializable)generateSerializationFriendlyMethods();else if (accidentallySerializable)generateSerializationHostileMethods();cw.visitEnd();// 这行代码调用 ClassWriter 对象的 toByteArray 方法,将动态生成的类转换为字节码数组。cw 是 ClassWriter 的实例,它负责生成类的字节码。final byte[] classBytes = cw.toByteArray();/**这段代码首先检查 dumper 对象是否为 null。dumper 是一个可能用于将字节码写入文件的工具对象。如果 dumper 不为 null,则执行以下步骤:1.使用 AccessController.doPrivileged 方法执行一个特权操作。这是因为写入文件可能需要特定的权限,特别是在启用了安全管理器的环境中。2.在 doPrivileged 方法中,执行一个 PrivilegedAction,其 run 方法调用 dumper.dumpClass 方法,将类名 lambdaClassName 和字节码数组 classBytes 传递给它,以便将字节码写入文件。3.doPrivileged 方法的第二个参数是 null,表示不使用特定的 AccessControlContext。4.第三和第四个参数是 FilePermission 和 PropertyPermission 对象,分别授予读写所有文件的权限和读取用户当前目录的权限。这些权限是执行文件写入操作所必需的。*/// 转储到文件if (dumper != null) {AccessController.doPrivileged(new PrivilegedAction<Void>() {@Overridepublic Void run() {dumper.dumpClass(lambdaClassName, classBytes);return null;}}, null,new FilePermission("<<ALL FILES>>", "read, write"),// 创建目录可能需要它new PropertyPermission("user.dir", "read"));}/**1.代码的作用下面这段代码的作用是在运行时动态定义一个匿名类。UNSAFE.defineAnonymousClass 方法接收三个参数:目标类(targetClass),类的字节码(classBytes),以及与类相关联的常量池补丁(这里传入的是 null)。2.代码的结构和逻辑2.1 targetClass:这是一个 Class 对象,表示新定义的匿名类将与之关联的上下文。通常,这个类是匿名类逻辑上的“宿主”类。2.2 classBytes:这是一个字节数组,包含了新匿名类的字节码。这些字节码通常是通过某种字节码生成库(如ASM)动态生成的。2.3 null:这个参数是用于类定义时的常量池补丁,这里传入 null 表示不需要进行常量池的补丁。3.关键代码块或语句的解释3.1 UNSAFE:这是 sun.misc.Unsafe 类的一个实例。Unsafe 类提供了一组底层、危险的操作,通常不推荐在标准Java代码中使用。但在某些特殊场景下,如动态类生成、低级并发控制等,Unsafe 提供的功能是必需的。3.2 .defineAnonymousClass(targetClass, classBytes, null):这个方法调用是动态定义匿名类的关键。它将 classBytes 中的字节码转换为一个Java类,并将这个新类与 targetClass 关联起来。由于这个类是匿名的,它没有正式的类名。传入的 null 参数表示在定义类的过程中不需要对常量池进行任何补丁操作。*/// 通过 Unsafe 类的 defineAnonymousClass 方法动态定义了一个匿名类,这个类的字节码由 classBytes 提供,而这个匿名类在逻辑上与 targetClass 关联。return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);}
四、利用java.lang.invoke.InnerClassLambdaMetafactory#dumper 转储lambda文件
// 静态初始化块,用于初始化dumperstatic {/*** 获取并设置代理类转储功能*/// 定义系统属性的键名,用于控制是否转储内部lambda代理类final String key = "jdk.internal.lambda.dumpProxyClasses";// 使用AccessController执行特权操作,获取系统属性值String path = AccessController.doPrivileged(new GetPropertyAction(key), // 创建获取属性的动作null, // 不指定AccessControlContextnew PropertyPermission(key , "read") // 指定所需的权限);// 根据获取的路径创建ProxyClassesDumper实例// 如果路径为null,则不启用转储功能dumper = (null == path) ? null : ProxyClassesDumper.getInstance(path);}
JVM参数:jdk.internal.lambda.dumpProxyClasses
命令:java -Djdk.internal.lambda.dumpProxyClasses ClassName
转储得到内部类:
反编译:java -jar cfr-0.152.jar LambdaExample.class --decodelambdas false
步骤一:源码
import java.util.Arrays;
import java.util.List;public class LambdaExample {public static void main(String[] args) {List<String> items = Arrays.asList("Apple", "Banana", "Cherry");items.forEach(item -> System.out.println(item));}
}
步骤二:编译,生成LambdaExample.class 文件
javac LambdaExample.java
步骤三:执行java命令,生成文件:LambdaExample$$Lambda$1.class
java -Djdk.internal.lambda.dumpProxyClasses LambdaExample
这个命令是用来调试和分析 Java 中 lambda 表达式的底层实现的。具体解释如下:
-
-D
参数:
用于设置系统属性。 -
jdk.internal.lambda.dumpProxyClasses
:
这是一个特殊的系统属性,用于指示 JVM 将 lambda 表达式生成的代理类保存到磁盘。 -
LambdaExample
:
这是要运行的包含 lambda 表达式的 Java 类名。
当你运行这个命令时,JVM 会执行以下操作:
- 运行
LambdaExample
类。 - 对于该类中的每个 lambda 表达式,JVM 会生成一个代理类。
- 这些生成的代理类会被保存到磁盘上,通常在当前工作目录下。
这个功能主要用于:
- 分析 lambda 表达式的底层实现
- 调试复杂的 lambda 表达式
- 了解 JVM 如何处理和优化 lambda 表达式
生成的代理类文件名通常遵循这样的模式:
主类名$Lambda$序号.class
步骤四:生成反编译代码:lambda内部类
java -jar cfr-0.152.jar 'LambdaExample$$Lambda$1.class' --decodelambdas false
mac电脑,此处LambdaExample$$Lambda$1.class需要带引号,因为在命令行中,$ 是一个特殊字符,用于引用变量。在这个上下文中,$$ 容易被解释为当前 shell 进程的 PID(进程ID),而不是文件名的一部分。所以你需要用引号将文件名括起来,这样可以防止 shell 解释 $ 字符。
/** Decompiled with CFR 0.152.*/
import java.lang.invoke.LambdaForm;
import java.util.function.Consumer;final class LambdaExample$$Lambda$1
implements Consumer {private LambdaExample$$Lambda$1() {}@LambdaForm.Hiddenpublic void accept(Object object) {LambdaExample.lambda$main$0((String)object);}
}
这段代码是由Java编译器为lambda表达式生成的内部类。让我们逐部分解析:
-
final class LambdaExample$$Lambda$1
- 这是一个自动生成的内部类,名称中的
$$Lambda$1
表示它是为第一个lambda表达式生成的。 final
关键字表示这个类不能被继承。
- 这是一个自动生成的内部类,名称中的
-
implements Consumer
- 这个类实现了
Consumer
接口,这是Java 8引入的函数式接口之一。
- 这个类实现了
-
private LambdaExample$$Lambda$1()
- 这是一个私有构造函数,防止外部直接实例化这个类。
-
@LambdaForm.Hidden
- 这是一个内部注解,用于标记这个方法不应该在堆栈跟踪中显示。
-
public void accept(Object object)
- 这是
Consumer
接口中定义的方法。 - 方法接受一个
Object
类型的参数。
- 这是
-
LambdaExample.lambda$main$0((String)object);
- 这行代码调用了
LambdaExample
类中的一个静态方法lambda$main$0
。 - 参数
object
被强制转换为String
类型。
- 这行代码调用了
这个生成的类实际上是lambda表达式的一个"包装器"。它将lambda表达式封装成一个实现了 Consumer
接口的具体类。当lambda表达式被调用时,它会调用 LambdaExample
类中相应的静态方法(在这里是 lambda$main$0
)。
这种实现方式允许Java在不使用匿名内部类的情况下支持lambda表达式,从而提高了性能和减少了内存使用。
三、结论
lambda 底层实现机制
1.lambda 表达式的本质:函数式接口的匿名子类的匿名对象
2.lambda表达式是语法糖
语法糖:编码时是lambda简洁的表达式,在字节码期,语法糖会被转换为实际复杂的实现方式,含义不变;即编码表面有个糖衣,在编译期会被脱掉
Lambda表达式的编译及运行过程如下:
编译阶段
-
Lambda表达式识别:
- 编译器识别Lambda表达式,将其转换为静态方法。
-
生成invokedynamic指令:
- 编译器为每个Lambda表达式生成一个invokedynamic指令。
- 指定LambdaMetafactory.metafactory或altMetafactory作为引导方法。
-
ASM使用:
- 编译器可能使用ASM库生成或修改字节码。
运行阶段
-
引导方法(Bootstrap Method)调用:
- 当JVM首次遇到某个
invokedynamic
指令时,它会调用指定的引导方法。对于Lambda表达式,这个引导方法通常是LambdaMetafactory
的metafactory
方法。
- 当JVM首次遇到某个
-
LambdaMetafactory调用:
- 引导方法(通常是LambdaMetafactory.metafactory)被调用。
- 接收参数:MethodHandles.Lookup、函数式接口信息、Lambda方法信息等。
-
创建
CallSite
对象:- 引导方法的任务之一是创建一个
CallSite
对象。CallSite
是一个抽象类,它代表了一个动态方法调用点。它的具体实现类(如ConstantCallSite
)封装了对特定方法的调用。 CallSite
对象持有一个MethodHandle
,这个MethodHandle
指向实际要执行的方法。对于Lambda表达式,这个方法是Lambda表达式转换成的方法。
- 引导方法的任务之一是创建一个
-
绑定
MethodHandle
:- 在创建
CallSite
对象时,引导方法会根据Lambda表达式的目标类型和实际代码,构造一个MethodHandle
。这个MethodHandle
直接指向了包含Lambda表达式代码的方法。 - 然后,这个
MethodHandle
被绑定到CallSite
对象上。这意味着,当通过这个CallSite
调用方法时,实际上是通过绑定的MethodHandle
来调用Lambda表达式对应的方法。
- 在创建
-
返回
CallSite
对象:- 引导方法返回
CallSite
对象给JVM。这个CallSite
对象随后被用于所有对该invokedynamic
指令的调用。 - 由于
CallSite
对象已经绑定了对应的MethodHandle
,因此每次通过这个CallSite
调用方法时,都会直接调用到Lambda表达式对应的方法,无需再次解析。
- 引导方法返回
-
InnerClassLambdaMetafactory使用:
InnerClassLambdaMetafactory
用于动态生成实现函数式接口的类。这个过程主要通过spinInnerClass
方法实现。 -
spinInnerClass
方法
spinInnerClass
方法的主要任务是动态生成一个类,这个类实现了指定的函数式接口,并包含了Lambda表达式的代码。这个方法通过直接操作字节码来创建类,通常使用ASM
库来完成。 -
使用
Unsafe
生成匿名类
spinInnerClass
方法可能会通过Unsafe
类的功能来加载生成的字节码。Unsafe
是JDK内部的一个类,提供了一些底层操作,比如直接内存访问、线程调度等。其中,Unsafe.defineAnonymousClass
方法可以用来加载一个类的字节码,并返回这个类的Class
对象。这个方法允许动态生成的类没有对应的.class
文件。 -
生成匿名类的过程
-
生成字节码:
spinInnerClass
方法使用ASM
库生成实现了函数式接口的类的字节码。这个类包含了Lambda表达式的实现代码。
-
加载类:
- 使用
Unsafe.defineAnonymousClass
方法加载生成的字节码。这个方法接受三个参数:父类的Class
对象、字节码数组、以及与类相关的常量池补丁。这个方法返回新加载的类的Class
对象。
- 使用
-
实例化:
- 通过反射或其他机制,使用返回的
Class
对象创建实例。这个实例实现了指定的函数式接口,并包含了Lambda表达式的代码。
- 通过反射或其他机制,使用返回的
-
-
绑定到
CallSite
:- 创建一个
MethodHandle
,指向新生成的类的实例方法。这个MethodHandle
随后被绑定到CallSite
对象上,用于后续的方法调用。
- 创建一个
注意
Unsafe
类的使用通常不推荐,因为它提供了很多强大但危险的底层操作。在JDK 9及以后版本中,Unsafe
类的一些功能被限制或替换,以促进更安全的编程实践。- JDK的具体实现细节可能会随着版本变化。上述过程主要描述了一种通过
Unsafe
加载动态生成类的方法,但实际的实现可能会有所不同。
- Lambda表达式执行:
- 当调用Lambda表达式时,通过CallSite间接调用动态生成的类中的方法。
相关文章:
lambda表达式底层实现
一、lambda 代码 & 反编译 原始Java代码 假设我们有以下简单的Java程序,它使用Lambda表达式来遍历并打印一个字符串列表: import java.util.Arrays; import java.util.List;public class LambdaExample {public static void main(String[] args) {…...
鸿蒙NEXT开发-组件事件监听和状态管理(基于最新api12稳定版)
注意:博主有个鸿蒙专栏,里面从上到下有关于鸿蒙next的教学文档,大家感兴趣可以学习下 如果大家觉得博主文章写的好的话,可以点下关注,博主会一直更新鸿蒙next相关知识 专栏地址: https://blog.csdn.net/qq_56760790/…...
《More Effective C++》的学习
引用与指针 没有所谓的null reference reference一定需要代表某个对象,所以C要求reference必须有初值。 QString &s; 使用reference可能比使用pointer更高效。 因为reference一定是有效的,而指针可能为空(需要多加一个判断࿰…...
Leetcode面试经典150题-322.零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。 你可以认为每种硬币的数量是无限的。 示…...
python17_len()函数
len()函数 A B "" C "hello world" D 18 E 18def len_test(s):try:# 尝试计算字符串的长度length len(s)return lengthexcept TypeError:# 如果不是字符串,则返回 None 或者提示错误return Noneif __name__ "__main__":# 单…...
车视界系统小程序的设计
管理员账户功能包括:系统首页,个人中心,汽车品牌管理,汽车颜色管理,用户管理,汽车信息管理,汽车订单管理系统管理 微信端账号功能包括:系统首页,汽车信息,我…...
SQLCMD命令行工具导入数据并生成对应的日志文件
SQLCMD是一个命令行工具,专门用于在Microsoft SQL Server数据库上运行SQL脚本和管理任务。它提供了一种交互式和自动化的方式来执行SQL命令和脚本,并允许用户与SQL Server数据库进行高效的交互。以下是关于SQLCMD的详细介绍: 主要功能 执行SQL脚本: SQLCMD可以执行包含SQL…...
tauri中加载本地文件图片或者下载网络文件图片后存储到本地,然后通过前端页面展示
有一个需求是需要将本地上传的文件或者网络下载的文件存储到本地,并展示在前端页面上的。其实如果只是加载本地文件,然后展示还是挺简单的,可以看我的文章:tauri程序加载本地图片或者文件在前端页面展示-CSDN博客 要想实现上述需…...
QSqlDatabase在多线程中的使用
Qt中多线程使用数据库_qt数据库管理类支持多数据库,多线程-CSDN博客 1. 代码: #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QPushButton> #include <QSqlDatabase> #include <QSqlQuery> #include <QSqlError>…...
【无人机设计与控制】Multi-UAV|多无人机多场景路径规划算法MATLAB
摘要 本研究探讨了多无人机路径规划问题,提出了三种不同算法的对比分析,包括粒子群优化(PSO)、灰狼优化(GWO)和鲸鱼优化算法(WOA)。利用MATLAB实现了多场景仿真实验,验证…...
Visual Studio C# 编写加密火星坐标转换
Visual Studio C# 编写加密火星坐标转换 1、WGS84坐标转GCJ02火星坐标2、GCJ02火星坐标转WGS84坐标(回归计算)3、GCJ02火星坐标转BD09百度坐标4、BD09百度坐标转GCJ02火星坐标(回归计算)5、坐标公共转换类6、地图显示7、程序简单界…...
微服务-流量染色
1. 功能目的 通过设置请求头的方式将http请求优先打到指定的服务上,为微服务开发调试工作提供便利 请求报文难模拟:可以直接在测试环境页面上操作,流量直接打到本地IDEA进行debug请求链路较长:本地开发无需启动所有服务…...
C语言实现 操作系统 经典的进程同步问题(2)
哲学家进餐问题 哲学家进餐问题是一个经典的同步问题,涉及多个哲学家试图同时用餐,但每个哲学家左右两边只有一把叉子。为了避免死锁和饥饿,可以使用记录型信号量(也称为计数信号量)来管理叉子的使用。 1、利用记录型…...
有效的字母异位词【字符串哈希】
题目 题解: 1.排序: #include<algorithm>class Solution{public:bool isAnagram(string s,string t){sort(s.begin(),s.end());sort(t.begin(),t.end());return st;} } 时间复杂度O(nlogn) 2.哈希表 #include<algorithm>int hash1[100]; …...
如何选择与运用工具提升工作效率的秘密指南
一、引言 ---- 在当今这个信息爆炸的时代,编程工具的选择对于开发者的工作效率至关重要。从智能的代码编辑器到强大的版本控制工具,再到那些能让我们事半功倍的自动化脚本,每一款工具都有其独特的优势和价值。那么,哪款编程工具…...
Spring系列 AOP实现过程
文章目录 实现原理EnableAspectJAutoProxyAnnotationAwareAspectJAutoProxyCreator 代理创建过程wrapIfNecessarygetAdvicesAndAdvisorsForBeanfindCandidateAdvisorsfindAdvisorsThatCanApply createProxy AspectJ注解处理代理调用过程 实现原理 本文源码基于spring-aop-5.3.…...
C语言 getchar 函数完全解析:掌握字符输入的关键
前言 在C语言中,getchar 是一个非常实用的函数,用于从标准输入流(通常是键盘)读取单个字符。这对于处理文本输入非常有用,尤其是在需要逐个字符处理的情况下。本文将深入探讨 getchar 函数的用法和特点,并…...
Docker安装mysql8并配置主从复制
1. 安装mysql8 1.1 新增挂载文件 # 新增mysql挂载文件夹 mkdir -p /root/docker/mysql/m01/log mkdir -p /root/docker/mysql/m01/data mkdir -p /root/docker/mysql/m01/conf1.2 新增mysql配置文件 # 新增mysql配置文件 cd /root/docker/mysql/m01/conf vim my.cnf # 下面是…...
快手:数据库升级实践,实现PB级数据的高效管理|OceanBase案例
本文作者:胡玉龙,快手技术专家 快手在较初期采用了OceanBase 3.1版本成功替换了多个核心业务、数百套的MySQL集群。至2023年,快手的数据量已突破800TB大关,其中最大集群的数据量更是达到了数百TB级别。为此,快手将数据…...
基于Node.js+Express+MySQL+VUE实现的计算机毕业设计共享单车管理网站
单车信息选择骑行 骑行状态留言公告/springboot/javaWEB/J2EE/MYSQL数据库/vue前后分离小程序 目录 功能图 界面展示 开发目标 开发背景意义 开发意义 开发目的 项目概述 技术选型与理由 系统设计与功能实现 项目可执行性分析 系统架构需求 性能需…...
人工智能辅助的神经康复
人工智能辅助的神经康复是通过应用人工智能(AI)技术来改善神经系统损伤患者的康复过程。此领域结合了深度学习、数据分析和机器人技术,旨在提升康复效果、个性化治疗方案和监测进展。以下是该领域的关键组成部分和应用: 1. 康复评…...
KKT实际运用 -MATLAB
FMINCON函数可以很方便的求出:fun:目标函数,即需要最小化的函数,输入参数为向量x,输出为标量f(x)。x0:初始点,即求解过程的起始点,可以是标量、向量或矩阵。A和b:线性不等…...
php在线相册
1、将静态页面效果完成 解压到www里 整个数据 暂时是错误的 建立连接密码为root 运行sql文件 右键根目录刷新 刷新后成功 开始 测试 如果需要上传照片,点击创建相册,选择上传文件,选择文件后退出 导入alumbenew2 2.提交表单方式 3.利用ph…...
Xcode手动安装SDK模拟器
1.下载SDK模拟器&Xcode SDK和Xcode官方下载地址 2.下载好后使用命令将SDK导入到Xcode中如下命令 注:我是在/Applications 目录下执行的命令,模拟其地址直接拖拽过来 sudo xcode-select -s Xcode.app xcodebuild -runFirstLaunch xcodebuild -imp…...
Docker安装consul + go使用consul + consul知识
1. 什么是服务注册和发现 假如这个产品已经在线上运行,有一天运营想搞一场促销活动,那么我们相对应的【用户服务】可能就要新开启三个微服务实例来支撑这场促销活动。而与此同时,作为苦逼程序员的你就只有手动去 API gateway 中添加新增的这…...
JWT 漏洞 - 学习手册
0x01:JWT 前导知识 0x0101:JWT 详解 0x02:JWT 漏洞介绍 0x0201:JWT 漏洞介绍 0x03:JWT 挖掘思路 JWT 漏洞挖掘思路 - JWT Payload 敏感信息泄露 备注:通过泄露的 JWT Payload 获取用户的敏感信息&#…...
HTML【知识改变命运】03font 字体标签
题目:在页面上显示"北京"两个字,字体为微软雅黑,颜色为红色,大小为40xp; font标签可以修饰字体的大小,颜色,和字体 属性:color颜色,face字体,size大…...
集师专属知识付费小程序搭建 心理咨询小程序搭建
一、产品简介 集师SaaS知识付费软件,为知识创业者或商家提供一站式内容交付解决方案,助力商家搭建集品牌传播、商业变现和用户运营于一体的线上知识服务系统,覆盖全渠道经营场景,占据每个流量入口,使流量变现快速高效…...
https://www.aitoolpath.com/ 一个工具数据库,目前储存了有2000+各种工具。每日更新
AI 工具爆炸?别怕,这个网站帮你整理好了! 哇塞,兄弟们!AI 时代真的来了!现在各种 AI 工具跟雨后春笋似的,噌噌噌地往外冒。AI 写作、AI 绘画、AI 代码生成……简直是要逆天啊! 可是…...
科技的成就(六十三)
583、八小时工作制 最先提出这种理念的人竟然也是一名企业家,而且还是一名空想社会主义者。这名叫做罗伯特欧文的英国人,也凭借先进的人本管理理念成为了现代人事管理之父。 584、SDN(软件定义网络) "SDN(软件定…...
青岛建设投资公司网站/厦门seo代理商
请问大家:没有网怎么设置路由器?答:电脑、手机 与路由器正确连接后,就可以打开路由器的设置界面,对路由器进行设置了。温馨提示:(1)、电脑/手机不能上网,并不会影响到对路由器的设置,…...
个人软件制作网站/站长之家查询网站
在部分的商用虚拟机(Sun HotSpot、IBM J9)中,Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”…...
做pc端网站报价/如何在百度上发布广告
1.根据php版本下载对应的swoole扩展版本进行编译安装,步骤安装官方手册进行安装即可2.创建一个异步服务器程序Server,只能用于php-cli环境,以thinkphp5为例复制public目录下的入口文件index.php到cli目录cli.php里4.application目录下创建cli…...
flash中国官网/桔子seo工具
今天在做jasper report生成pdf文档的时候,需要引入亚洲字体jar包。maven仓库是有这个jar包,但是在项目pom文件始终不能下载。无奈只有将jar包安装到maven本地仓库。 1 将jar包放到电脑的某个位置,如:C:\E\Received\FireFox\itext-…...
北京网站设计培训班/电商关键词排名优化怎么做?
真的是书到用时方恨少!几个月前,毕业的时候大把的书被按斤卖掉,现在想找一本数电书再复习一下触发器都变得难,又不舍得买。。。。今天遇到一个解码编码器的触发器鉴相电路,看到D触发器一下蒙了,似乎懂&…...
织梦培训机构网站模板/百度一下官方网页版
Spring Cloud并不是一个框架,而是很多技术的统称...