深入理解JDK动态代理原理,使用javassist动手写一个动态代理框架

news2024/10/7 15:57:42

文章目录

  • 一、动手实现一个动态代理框架
    • 1、初识javassist
    • 2、使用javassist实现一个动态代理框架
  • 二、JDK动态代理
    • 1、编码实现
    • 2、基本原理
      • (1)getProxyClass0方法
      • (2)总结
  • 写在后面

一、动手实现一个动态代理框架

1、初识javassist

Java操纵字节码,最底层一般是使用ASM进行操作的,但是ASM上手难度很大,我们可以使用javassist对字节码进行操作,相对来说简单一些。

下面这个实例,就是我们使用javassist对接口动态生成一个实现类:


import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

public class JavassistDemo {


    public static void main(String[] args) throws Exception {

        TestService proxy = createProxy();
        proxy.sayHello("zhangsan"); // hello:zhangsan
    }

    /**
     * 生成一个TestService的实现类
     */
    public static TestService createProxy() throws Exception {
        // javassist 底层是ASM,ASM底层是编辑JVM指令码
        ClassPool classPool = new ClassPool();
        // 添加classLoader
        classPool.appendSystemPath();
        // 1.创建一个类
        CtClass class1 = classPool.makeClass("TestServiceImpl");
        class1.addInterface(classPool.get(TestService.class.getName()));
        // 2.创建一个方法
        CtMethod satHelloMethod = CtNewMethod.make(CtClass.voidType, // void返回值
                "sayHello", // 方法名
                new CtClass[]{classPool.get(String.class.getName())}, // 方法参数
                new CtClass[0], // 异常类型
                "{System.out.println(\"hello:\"+$1);}", // 方法体内容,$1表示第一个参数
                class1 // 指定类
                );
        class1.addMethod(satHelloMethod);
        // 3.实例化这个对象
        Class aClass = classPool.toClass(class1);
        // 强制转换
        return (TestService) aClass.newInstance();
    }

    public interface TestService {
        void sayHello(String name);
    }
}

2、使用javassist实现一个动态代理框架


import javassist.*;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.stream.Collectors;

public class Javassist3Demo {

    public static void main(String[] args) throws Exception {

        TestService proxy = createProxy(TestService.class, new InvocationHandler() {
            @Override
            public Object invoke(String methodName, Object[] args) {
                // 根据方法判断逻辑
                if(methodName.equals("sayHello3")) {
                    System.out.println("hello" + args[0]);
                    return "aa";
                } else {
                    System.out.println("hello2" + args[0]);
                    return "aa2";
                }
            }
        });
        proxy.sayHello("zhangsan"); // hellozhangsan
        proxy.sayHello2("zz", 1);
        System.out.println(proxy.sayHello3("qq"));
    }

    static int count = 0;
    public static <T> T createProxy(Class<T> classInterface, InvocationHandler handler) throws Exception {
        ClassPool classPool = new ClassPool();
        classPool.appendSystemPath();
        // 1.创建一个类
        CtClass impl = classPool.makeClass("$Proxy" + count ++);
        impl.addInterface(classPool.get(classInterface.getName()));

        // 2.impl类中 添加属性handler
        CtField fie = CtField.make("public com.mydemo.Javassast3Demo.InvocationHandler handler=null;", impl);
        impl.addField(fie);

        // 有返回值类型和无返回值类型的源码
        String src = "return ($r)this.handler.invoke(\"%s\", $args);"; // $args获取所有参数
        String voidSrc = "this.handler.invoke(\"%s\",$args);";

        for (Method method : classInterface.getMethods()) {
            CtClass returnType = classPool.get(method.getReturnType().getName());
            String name = method.getName();
            CtClass[] parameters = toCtClass(classPool, method.getParameterTypes());
            CtClass[] errors = toCtClass(classPool, method.getExceptionTypes());

            // 2.创建一个方法
            CtMethod newMethod = CtNewMethod.make(returnType, // 返回值
                    name, // 方法名
                    parameters, // 方法参数
                    errors, // 异常类型
                    method.getReturnType().equals(Void.class) ? String.format(voidSrc, method.getName()) : String.format(src, method.getName()), // 方法体内容
                    impl // 指定类
            );
            impl.addMethod(newMethod);

        }

        // 生成字节码(辅助学习用)
        //byte[] bytes = impl.toBytecode();
        //Files.write(Paths.get(System.getProperty("user.dir") + "/target/" + impl.getName() + ".class"), bytes);

        // 3.实例化这个对象
        Class aClass = classPool.toClass(impl);
        T t = (T) aClass.newInstance();
        aClass.getField("handler").set(t, handler); // 初始化赋值
        // 强制转换
        return t;
    }


    private static CtClass[] toCtClass(ClassPool pool, Class[] classes) {
        return Arrays.stream(classes).map(c -> {
            try {
                return pool.get(c.getName());
            } catch (NotFoundException e) {
                throw new RuntimeException(e);
            }
        }).collect(Collectors.toList()).toArray(new CtClass[0]);
    }

    public interface InvocationHandler {
        Object invoke(String methodName, Object args[]);
    }

    public class InvocationHandlerImpl implements InvocationHandler {

        @Override
        public Object invoke(String methodName, Object[] args) {
            System.out.println("hello");
            return null;
        }
    }

    public interface TestService {
        void sayHello(String name);
        void sayHello2(String name, Integer id);
        String sayHello3(String name);
    }
}

这样,我们只要实现InvocationHandler 接口,就可以自定义代理类的核心逻辑了,我们对生成的代理类进行反编译:

public class $Proxy0 implements TestService {
    public InvocationHandler handler = null;

    public String sayHello3(String var1) {
        return (String)this.handler.invoke("sayHello3", new Object[]{var1});
    }

    public void sayHello(String var1) {
        this.handler.invoke("sayHello", new Object[]{var1});
    }

    public void sayHello2(String var1, Integer var2) {
        this.handler.invoke("sayHello2", new Object[]{var1, var2});
    }

    public $Proxy0() {
    }
}

实现了我们的接口,并且重写了接口的所有方法,最终执行的是InvocationHandler的invoke方法。

这也正是JDK动态代理的基本思想。

二、JDK动态代理

1、编码实现

public interface EchoService {
    String echo(String message) throws NullPointerException;
}

public class DefaultEchoService implements EchoService {
    @Override
    public String echo(String message) {
        return "[ECHO] " + message;
    }
}

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * JDK动态代理实例
 */
public class JDKDynamicProxyDemo {

    public static void main(String[] args) {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        // 真实的对象
        DefaultEchoService realObj = new DefaultEchoService();

        // 代理的对象
        Object proxy = Proxy.newProxyInstance(classLoader, new Class[]{EchoService.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("动态前置");
                Object obj = null;
                if (EchoService.class.isAssignableFrom(method.getDeclaringClass())) {
                    // 执行真实方法
                    obj = method.invoke(realObj, args);
                }
                System.out.println("动态后置");
                return obj;
            }
        });
        EchoService echoService = (EchoService) proxy;
        System.out.println(echoService.echo("Hello,World"));
    }
}

我们发现,使用JDK动态代理,基本逻辑和我们上面使用javassist手写的工具类差不多,只不过JDK动态代理底层是使用更复杂的方式实现的,我们这里取巧,使用javassist实现。

注意!这里需要DefaultEchoService 实现EchoService接口,代理的其实是EchoService接口而不是DefaultEchoService 类。

动态代理对原代码没有侵入性,通常可以动态加载。

2、基本原理

为什么 Proxy.newProxyInstance 会生成新的字节码?

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

Object proxy = Proxy.newProxyInstance(classLoader, new Class[]{EchoService.class}, new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }
});

System.out.println(proxy.getClass());// com.sun.proxy.$Proxy0


Object proxy2 = Proxy.newProxyInstance(classLoader, new Class[]{Comparable.class}, (proxy1, method, args1) -> {
            return null;
        });

System.out.println(proxy2.getClass());// com.sun.proxy.$Proxy1

上面代码我们会发现,Java动态代理每生成一个代理,它的class总是com.sun.proxy包下的$Proxy*,从0开始累加,它是如何实现的呢?

我们来分析一下Proxy的newProxyInstance方法:

// java.lang.reflect.Proxy#newProxyInstance
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    Objects.requireNonNull(h);
	// 对象克隆
    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
     * Look up or generate the designated proxy class.
     */
     // 先从缓存获取(见 (1))
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }
		// 获取代理对象的构造方法,带着InvocationHandler参数的构造方法
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        // 返回Proxy对象
        return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {
        throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
        Throwable t = e.getCause();
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else {
            throw new InternalError(t.toString(), t);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
}

(1)getProxyClass0方法

在getProxyClass0方法中,从proxyClassCache缓存中获取了这个代理类:

// java.lang.reflect.Proxy#getProxyClass0
private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // If the proxy class defined by the given loader implementing
    // the given interfaces exists, this will simply return the cached copy;
    // otherwise, it will create the proxy class via the ProxyClassFactory
    return proxyClassCache.get(loader, interfaces);
}

而proxyClassCache在初始化时,自动创建了KeyFactory和ProxyClassFactory

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
        proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

ProxyClassFactory的核心方法apply,隐藏着代理接口的创建逻辑:

// java.lang.reflect.Proxy.ProxyClassFactory
private static final class ProxyClassFactory
    implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
    // prefix for all proxy class names
    private static final String proxyClassNamePrefix = "$Proxy";

    // next number to use for generation of unique proxy class names
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        for (Class<?> intf : interfaces) { // 遍历我们传入的接口数组
            /*
             * Verify that the class loader resolves the name of this
             * interface to the same Class object.
             */
            Class<?> interfaceClass = null;
            try {
            	// 通过classLoader加载我们的接口
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + " is not visible from class loader");
            }
            /*
             * Verify that the Class object actually represents an
             * interface.
             */
            if (!interfaceClass.isInterface()) {// 只能代理接口,非接口直接抛异常
                throw new IllegalArgumentException(
                    interfaceClass.getName() + " is not an interface");
            }
            /*
             * Verify that this interface is not a duplicate.
             */
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                    "repeated interface: " + interfaceClass.getName());
            }
        }

        String proxyPkg = null;     // package to define proxy class in
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

        /*
         * Record the package of a non-public proxy interface so that the
         * proxy class will be defined in the same package.  Verify that
         * all non-public proxy interfaces are in the same package.
         */
        for (Class<?> intf : interfaces) {
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                accessFlags = Modifier.FINAL;
                String name = intf.getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        "non-public interfaces from different packages");
                }
            }
        }

        if (proxyPkg == null) { // 包名就是com.sun.proxy
            // if no non-public proxy interfaces, use com.sun.proxy package
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        /*
         * Choose a name for the proxy class to generate.
         */
        long num = nextUniqueNumber.getAndIncrement();
        String proxyName = proxyPkg + proxyClassNamePrefix + num; // 依次递增

        /*
         * Generate the specified proxy class.
         */
         // 代理类生成器,返回字节数组,就是字节码
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
        try {
        	// classLoader加载类,是一个native方法,返回一个Class对象
            return defineClass0(loader, proxyName,
                                proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            /*
             * A ClassFormatError here means that (barring bugs in the
             * proxy class generation code) there was some other
             * invalid aspect of the arguments supplied to the proxy
             * class creation (such as virtual machine limitations
             * exceeded).
             */
            throw new IllegalArgumentException(e.toString());
        }
    }
}

(2)总结

vm options参数设置-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true,就可以把生成的代理类的源码保存在com.sun.proxy目录下面,或者用arthas来查看运行中的类信息。

JDK动态代理生成的代理类,我们通过反编译,发现其实是这个样子的:

package com.sun.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import com.demo.EchoService;

public final class $Proxy0
extends Proxy
implements EchoService {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.demo.EchoService").getMethod("echo", Class.forName("java.lang.String"));
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            return;
        }
        catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
        }
        catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
        }
    }

    public final boolean equals(Object object) {
        try {
            return (Boolean)this.h.invoke(this, m1, new Object[]{object});
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final String toString() {
        try {
            return (String)this.h.invoke(this, m2, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final int hashCode() {
        try {
            return (Integer)this.h.invoke(this, m0, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final String echo(String string) throws NullPointerException {
        try {
            return (String)this.h.invoke(this, m3, new Object[]{string});
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

InvocationHandler就是Proxy.newProxyInstance传入的最后一个参数。

当调用代理对象的方法时,会执行InvocationHandler的invoke方法。

注意,JDK生成的代理类的包名不总是com.sun.proxy,只有当接口为Public时是这样的,当接口为非public时,生成的代理类与接口所在包名相同。

写在后面

如果本文对你有帮助,请点赞收藏关注一下吧 ~
在这里插入图片描述

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

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

相关文章

Dijkstra算法的入门与应用

目录 一、前言 二、Dijkstra算法 1、Dijkstra 算法简介 2、算法思想&#xff1a;多米诺骨牌 3、算法实现 4、例子 三、例题 1、蓝桥王国&#xff08;lanqiaoOJ题号1122&#xff09; 一、前言 本文主要讲了Dijkstra算法的概念、实现与一道模板例题。 二、Dijkstra算法…

RSTP基础要点(上)

RSTP基础RSTP引入背景STP所存在的问题RSTP对于STP的改进端口角色重新划分端口状态重新划分快速收敛机制&#xff1a;PA机制端口快速切换边缘端口的引入RSTP引入背景 STP协议虽然能够解决环路问题&#xff0c;但是由于网络拓扑收敛较慢&#xff0c;影响了用户通信质量&#xff…

分布式对象存储

参考《分布式对象存储----原理、架构以及Go语言实现》&#xff08;作者&#xff1a;胡世杰&#xff09; 对象存储简介 数据的管理方式 以对象的方式管理数据&#xff0c;一个对象包括&#xff1a;对象的数据、对象的元数据、对象的全局唯一标识符 访问数据的方式 可扩展的分…

useCallback、useMemo、React.memo

1、React.memo React.memo 是 React 中用于函数组件优化的高阶组件&#xff0c;可以在一定程度上减少组件的重渲染&#xff0c;提升应用性能。React.memo 的实现原理是对比组件的前后两次渲染传入的 props 是否相等&#xff0c;如果相等则不会触发重新渲染&#xff0c;否则会触…

使用 Nacos 搭建一个简单的微服务项目

Nacos Nacos 是阿里巴巴推出来的一个新开源项目&#xff0c;一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 准备Nacos 将 nacos 安装成功之后&#xff0c;进入nacos的bin 目录下&#xff0c;通过命令sh startup.sh -m standalone启动nacos&#xff0c;然后…

ChatGPT概述:从模型训练到基本应用的介绍

ChatGPT概述&#xff1a;从模型训练到基本应用的介绍 目录 本文是对ChatGPT的由来、训练过程以及实际落地场景的解释&#xff0c;主要内容包括如下三个方面&#xff1a; 1、ChatGPT是什么 2、ChatGPT的原理 3、ChatGPT的思考 4、ChatGPT的应用 ChatGPT是什么 ChatGPT可能是近…

代码随想录算法训练营第四天| 24. 两两交换链表中的节点 、19.删除链表的倒数第N个节点、面试题 02.07. 链表相交 、142.环形链表II

24. 两两交换链表中的节点 24.两两交换链表中的节点介绍给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。思路上述是自己看到这道…

Zookeeper3.5.7版本——客户端命令行操作(命令行语法)

目录一、命令行语法二、help命令行语法示例一、命令行语法 命令行语法列表 命令基本语法功能描述help显示所有操作命令ls path使用 ls 命令来查看当前 znode 的子节点 [可监听]-w 监听子节点变化-s 附加次级信息create普通创建-s 含有序列-e 临时&#xff08;重启或者超时消失…

【3.5】单调栈、回文数、子序列、编辑距离问题,MySQL、Redis

文章目录单调栈总结子序列问题总结编辑距离问题总结回文串问题总结MySQL 执行流程是怎样的&#xff1f;第一步&#xff1a;连接器第二步&#xff1a;查询缓存第三步&#xff1a;解析器 解析SQL第四步&#xff1a;执行 SQL2.2 MySQL 一行记录是怎么存储的&#xff1f;MySQL 的数…

ChatGPT vs Bard 背后的技术对比分析和未来发展趋势

ChatGPT vs Bard 背后的技术对比分析和未来发展趋势 目录 ChatGPT vs Bard 背后的技术对比分析和未来发展趋势

Vulnhub系列:VulnOSv2

老样子&#xff0c;kali ip:192.168.56.104&#xff0c;靶机ip利用nmap或arp-scan -l进行查看靶机ip为&#xff1a;192.168.56.124&#xff0c;利用nmap进行端口探测发现了22、80、6667端口&#xff0c;下一步就是进行web探测&#xff0c;输入靶机ip后发现页面存在个链接&#…

Qt中调用gtest进行单元测试及生成覆盖率报告

一.环境配置 googletest地址:https://github.com/google/googletest 我下载的是1.12.1,这是最后一个支持C++11的版本。 首先编译gtest,在windows上的编译方式和编译gRPC一模一样,详见Qt中调用gRPC,编译完了会生成几个静态库,如下图所示 本文主要用到了libgtest.a 下载ms…

多线程二 多线程了解与使用

文章目录synchronized 锁有两种synchronized异常捕获主线程和子线程volatile的作用notify是随机启动等待线程中的一个synchronized 锁有两种 类对象类的实例 第一种&#xff1a;锁类对象&#xff0c;有两种方式&#xff0c;如下&#xff1a; // 方法一&#xff1a;synchroni…

Dubbo源码解析-——服务导出

前言 在之前我们讲过Spring和Dubbo的集成&#xff0c;我们在服务上标注了DubboService的注解&#xff0c;然后最终Dubbo会调用到ServiceBean#export方法中&#xff0c;本次我们就来剖析下服务导出的全流程。 一、前置回顾 由于ServiceBean实现了ApplicationListener接口&…

基于图像识别的数据处理系统

基于EASYDL模型的图像识别数据处理系统 需求分析 1.1软件背景分析 世界已经进入工业自动化的时代。随着图像识别、语音识别、机械稳定化的发展。自动化已经成为公司或者企业发展的重要方向。自动化是指机器设备或生产过程在不需要人工直接干预情况下&#xff0c;按照预期的目…

Java分布式解决方案(二)

文章目录&#x1f525;分布式事务处理_认识本地事务&#x1f525;关系型数据库事务基础_并发事务带来的问题&#x1f525;关系型数据库事务基础_MySQL事务隔离级别&#x1f525;MySQL事务隔离级别_模拟异常发生之脏读&#x1f525;MySQL事务隔离级别_模拟异常发生之不可重复读&…

浏览器渲染原理

阶段 - Parse 1、解析HTML&#xff0c;浏览器将从服务器获取到的HTML文件之后&#xff0c;会产生一个渲染任务&#xff0c;交给消息队列&#xff08;EventLoop/MessageLoop&#xff09;。 2、在事件循环机制的作用下&#xff0c;会将渲染任务交给主线程 3、主线程在获取到渲染…

入门vue(1-10)

正确学习方式&#xff1a;视频->动手实操->压缩提取->记录表述 1基础结构 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"&…

LeetCode 1599. 经营摩天轮的最大利润

【LetMeFly】1599.经营摩天轮的最大利润 力扣题目链接&#xff1a;https://leetcode.cn/problems/maximum-profit-of-operating-a-centennial-wheel/ 你正在经营一座摩天轮&#xff0c;该摩天轮共有 4 个座舱 &#xff0c;每个座舱 最多可以容纳 4 位游客 。你可以 逆时针 轮…

0103深度优先搜索和单点连通-无向图-数据结构和算法(Java)

文章目录1.1 走迷宫1.2 图的深度优先搜索实现1.3 算法分析及性能1. 4 单点连通性后记1.1 走迷宫 简单的迷宫&#xff0c;如下图1.1-1所示&#xff1a; 探索迷宫而不迷路&#xff0c;我们需要&#xff1a; 选择一条没有标记过的通道&#xff0c;在你走过的路上铺一条绳子&…