Cglib动态代理从入门到掌握

Cglib 动态代理

本文的写作目的是为了探究 Spring 框架中在使用@Transactional标注的方法中使用 this 进行自调用时事务失效的原因,各种视频教程中只是简单指出 this 指向的不是代理类对象,而是目标类对象,但是并没有解释为什么 this 不是代理类对象?

在学习完 JDK 动态代理之后,我认为是动态代理的原因。虽然知道 Cglib Proxy 和 JDK Proxy 的实现原理不同,但当时认为方法调用只能通过 invoke 进行反射调用(错误依据),而传递给 invoke 方法的对象就是目标类对象,因此 this 指向的就是传递过来的目标类对象。具体可以查看另一篇博客。

最近学习完 Cglib 动态代理之后,发现动态代理类进行方法调用并不是只能依靠反射调用的,因此那一篇博客的分析也就不成立了。先说结论,在 Cglib 动态代理中,由于绕开了反射调用方法,所以 this 既可以指向代理类对象,也可以和 JDK 动态代理一样指向目标类对象,而 Spring 框架中选择了后者,从而有了 this 造成事务失效的情况。但是就 Cglib 本身实现动态代理而言,这个问题是可以避免的。

依赖环境

下面两个 pom 依赖二选一即可

<!--spring-core中包含cglib-->
<dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>5.3.30</version>
</dependency><dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.1</version>
</dependency>

案例演示

public class CglibProxyTest {private static final String CLASSPATH = ClassLoader.getSystemResource("").getPath().substring(1);public static void main(String[] args) {// 设置生成字节码文件System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, CLASSPATH);MethodInterceptor methodInterceptor = new MyMethodInterceptor();Enhancer enhancer = new Enhancer();// 设置父类字节码enhancer.setSuperclass(ServiceImpl.class);// 设置增强方法(即方法拦截器)// TODO: 从代理类的源码中可以看出来,setCallbacks只会使用到第一个拦截器,那么setCallbacks方法有什么意义呢?enhancer.setCallback(methodInterceptor);ServiceImpl serviceImpl = (ServiceImpl) enhancer.create();serviceImpl.show("Hello World");// 测试cglib代理方式下的this自调用和Spring事务的this自调用的区别//serviceImpl.getMsg(1, 2);}
}class MyMethodInterceptor implements MethodInterceptor {/*** 拦截方法** @param proxy        代理类对象* @param targetMethod 目标类中的方法对象* @param args         方法参数* @param methodProxy  Cglib底层使用到的MethodProxy对象,并不是代理方法* @return* @throws Throwable*/@Overridepublic Object intercept(Object proxy, Method targetMethod, Object[] args, MethodProxy methodProxy) throws Throwable {System.out.println("proxy.getClass() = " + proxy.getClass());System.out.println("targetMethod.getName() = " + targetMethod.getName());System.out.println("targetMethod.getDeclaringClass().getName() = " + targetMethod.getDeclaringClass().getName());System.out.println("===========targetMethod before==========");// 可能出现的情况//  情况一:methodProxy.invoke(proxy, args) 陷入invoke -> intercept -> invoke ->...的无限递归中////  情况二:targetMethod.invoke(proxy, args) 陷入 intercept->show->intercept的死循环中。//  这是因为targetMethod是一个Method方法对象,只有invoke方法,此时没有涉及到MethodProxy的invoke和invokeSuper方法//  而proxy是targetClass的一个子类对象,因此targetMethod.invoke(proxy, args)相当于在调用proxy对象中的同名targetMethod方法,即增强方法。所以陷入死循环// Spring使用this无法增强的原因,使用下面的情况三方案,其中target对象是外部传入的,和MethodInvocationHandler一样// 情况三:methodProxy.invoke(target, args)//Object result = methodProxy.invokeSuper(proxy, args);System.out.println("===========targetMethod after===========");return result;}
}/*** 目标类(被代理类)不存在接口*/
class ServiceImpl {public void show(String msg) {System.out.println(msg);}public String getMsg(int x, int y) {this.show("Hello World");return String.valueOf(x + y);}
}

代理类和目标类的关系结构图

在这里插入图片描述

图:Cglib动态代理中代理类与目标类之间的关系

代理类字节码

对反编译后的源代码文件进行变量名的调整,删减一些非核心的细节内容。

从重写的增强方法中可以看出来,传递给 intercept() 方法的参数的含义分别是:

  1. Object:(ServiceImplCglibProxy)代理类对象(this)
  2. Method:代理类中对于目标类方法的对象引用,即上图中的 Method01Method02Method03
  3. Object[]:方法参数数组(args)
  4. MethodProxy:根据代理类中的 cglibMethod0x()method0x() 生成的 MethodProxy 对象
public class ServiceImplCglibProxyextends ServiceImplimplements Factory {// 目标类中的的方法对象(show)private static Method showMethod;// 代理类中show方法和cglibShow方法共同构建的MethodProxy对象private static MethodProxy showMethodProxy;// 目标类中的的方法对象(show)private static Method getMsgMethod;// 代理类中getMsg方法和cglibGetMsg方法共同构建的MethodProxy对象private static MethodProxy getMsgMethodProxy;// 用来包装Object[0]private static final Object[] emptyArgs = new Object[0];// Object类中的方法private static Method finalizeMethod;private static MethodProxy finalizeMethodProxy;// 判断是否绑定过ThreadLocal中的Callback,如果绑定过,那么之后就不需要绑定了private boolean bound;// 自定义增强逻辑,MethodInterceptor是Callback的一个子接口,在实例化的时候会通过调用静态方法进行设置,省略private MethodInterceptor methodInterceptor;static {init();}@SneakyThrowsstatic void init() {// 获取当前代理类的字节码对象Class proxyClass = Class.forName("org.example.ServiceImplCglibProxy");Method[] methods;// 处理Object类中的所有方法Class targetClass = Class.forName("java.lang.Object");methods = ReflectUtils.findMethods(new String[]{"finalize", "()V","equals", "(Ljava/lang/Object;)Z","toString", "()Ljava/lang/String;","hashCode", "()I","clone", "()Ljava/lang/Object;"},targetClass.getDeclaredMethods());finalizeMethod = methods[0];finalizeMethodProxy = MethodProxy.create(targetClass, proxyClass, "()V", "finalize", "cglibFinalize");// 省略 Object 类中的其他方法的处理...// 处理ServiceImpl(父类)中的所有方法targetClass = Class.forName("org.example.ServiceImpl");methods = ReflectUtils.findMethods(new String[]{"show", "(Ljava/lang/String;)V","getMsg", "(II)Ljava/lang/String;"},targetClass.getDeclaredMethods());// 处理ServiceImpl中的show方法showMethod = methods[0];showMethodProxy = MethodProxy.create(targetClass, proxyClass, "(Ljava/lang/String;)V", "show", "cglibShow");// 处理ServiceImpl中的getMsg方法getMsgMethod = methods[1];getMsgMethodProxy = MethodProxy.create(targetClass, proxyClass, "(II)Ljava/lang/String;", "getMsg", "cglibGetMsg");}public static MethodProxy findMethodProxy(Signature signature) {String methodSignature = signature.toString();// 先比较hashCode值,再比较字符串switch (methodSignature.hashCode()) {case -1574182249:if (methodSignature.equals("finalize()V")) {return finalizeMethodProxy;}break;case 550733602:if (methodSignature.equals("show(Ljava/lang/String;)V")) {return showMethodProxy;}break;case 351083702:if (methodSignature.equals("getMsg(II)Ljava/lang/String;")) {return getMsgMethodProxy;}break;}return null;}// 为便于区分,称为cglib方法void cglibShow(String msg) {super.show(msg);}// 为便于区分,称为重写方法@SneakyThrows@Overridepublic void show(String msg) {if(methodInterceptor == null){// 当使用methodProxy.invoke(target, args),由于target没有methodInterceptor,所有会进入到这个逻辑分支中super.show(msg);return;}// 将参数封装成Object数组,调用intercept方法// 在正常传递MethodInterceptor的情况下,会调用该方法methodInterceptor.intercept(this, showMethod, new Object[]{msg}, showMethodProxy);}// 为便于区分,称为cglib方法String cglibGetMsg(int x, int y) {return super.getMsg(x, y);}// 为便于区分,称为重写方法@SneakyThrows@Overridepublic String getMsg(int x, int y) {if(methodInterceptor == null){return super.getMsg(x, y);}return (String) methodInterceptor.intercept(this, getMsgMethod, new Object[]{x, y}, getMsgMethodProxy);}}

增强方法的调用流程图

在进一步讨论增强方法的调用流程之前,先重新查看 MethodInterceptor 的 intercept() 方法。我们可以得到四个参数,那么怎么选择来达成我们调用原始方法的目的。

结论:

  • 在不考虑引入额外的目标类对象 target,仅使用 intercept 方法中提供的四个参数的情况下,只有 invokeSuper() 方法能够正确执行,同时 this 指针指向的是 ServiceImplCglibProxy(代理类)对象,因为不会出现像 Spring 中使用 @Transactional 标注的方法中使用 this 会造成事务失效的问题。
  • 在使用额外引入的目标类对象 target 的情况下(和 JDK 动态代理的 InvocationHandler 类似,但 JDK Proxy 强制要求一个目标类对象,而 Cglib Proxy 并不要求目标类对象),除了 invokeSuper() 方法外,另外两个方法都可以正确执行。
  • **使用 MethodProxy 对象和 Method 对象的区别在于,MethodProxy 借助额外生成的字节码 FastClass 来实现对方法的直接调用,而 Method 则是通过反射来调用方法。**因此 MethodProxy 方式调用可以解决 Spring 的 this 造成的事务失效问题,但 Spring 是故意设计成和 JDK Proxy 的效果一样,为此还舍弃 invokeSuper 的调用方式,而是引入 target 来使用 invoke 调用。这样设计的官方解释没有仔细了解,个人分析,为了统一结果,因为 JDK Proxy 是通过反射来实现的,因此 this 只能代表目标类对象;如果仅仅因为使用的动态代理不同,就造成有的时候 this 失效,有的时候 this 有效,那么就凭空多添加了混乱。因此还不如都设计成 this 失效。
class MyMethodInterceptor implements MethodInterceptor {// 不是必要的,Spring注入target造成this无法调用自身对象,直接使用proxy对象就不会出现这种情况private Object target;@Overridepublic Object intercept(Object proxy, Method targetMethod, Object[] args, MethodProxy methodProxy) throws Throwable {System.out.println("===========targetMethod before==========");// 由于FastClass的机制,这里不能调用methodProxy.invoke(proxy, args),否则// 正常可能出现的情况//  情况一:methodProxy.invoke(proxy, args) 陷入invoke -> intercept -> invoke ->...的无限递归中////  情况二:targetMethod.invoke(proxy, args) 陷入 intercept->show->intercept的死循环中。//  这是因为targetMethod是一个Method方法对象,只有invoke方法,此时没有涉及到MethodProxy的invoke和invokeSuper方法//  而proxy是targetClass的一个子类对象,因此targetMethod.invoke(proxy, args)相当于在调用proxy对象中的同名targetMethod方法,即增强方法。所以陷入死循环// Spring使用this无法增强的原因,使用下面的情况三方案,其中target对象是外部传入的,和MethodInvocationHandler一样// 情况三:methodProxy.invoke(target, args)//Object result = methodProxy.invokeSuper(proxy, args);System.out.println("===========targetMethod after===========");return result;}
}

在这里插入图片描述

图:增强方法调用流程

MethodProxy

主要关注 invoke() 方法和 invokeSuper() 方法,因为这两个方法会在 MethodInterceptor 对象的 intercept() 方法中被我们调用来达到调用目标类中的原始方法的目的。

public class MethodProxy {private Signature overrideMethodSignature;private Signature cglibMethodSignature;// create方法中生成,init方法中清空private CreateInfo createInfo;private final Object initLock = new Object();private volatile FastClassInfo fastClassInfo;public static MethodProxy create(Class targetClass, Class proxyClass, String desc, String overrideMethodName, String cglibMethodName) {MethodProxy proxy = new MethodProxy();proxy.overrideMethodSignature = new Signature(overrideMethodName, desc);proxy.cglibMethodSignature = new Signature(cglibMethodName, desc);proxy.createInfo = new CreateInfo(targetClass, proxyClass);return proxy;}public Object invoke(Object object, Object[] args) throws Throwable {this.init();FastClassInfo fci = this.fastClassInfo;// 默认情况下 object 是 proxyClass 类型,那么 overrideMethod -> intercept -> invoke -> overrideMethod 形成死循环。// 如果按照Spring的方式手动传递 targetClass 类型的对象,object是targetClass类型(目标类)return fci.targetFastClass.invoke(fci.overrideMethodIndex, object, args);}public Object invokeSuper(Object proxy, Object[] args) throws Throwable {this.init();FastClassInfo fci = this.fastClassInfo;// 重写父类方法时,留了一份拷贝,称之为cglibMethod,因此可以在当前类直接调用父类中的同名方法return fci.proxyFastClass.invoke(fci.cglibMethodIndex, proxy, args);}private void init() {if (this.fastClassInfo == null) {synchronized (this.initLock) {if (this.fastClassInfo == null) {CreateInfo ci = this.createInfo;FastClassInfo fci = new FastClassInfo();fci.targetFastClass = helper(ci, ci.targetClass);fci.proxyFastClass = helper(ci, ci.proxyClass);// overrideMethodSignature在targetClass和proxyClass中的签名都相同fci.overrideMethodIndex = fci.targetFastClass.getIndex(this.overrideMethodSignature);fci.cglibMethodIndex = fci.proxyFastClass.getIndex(this.cglibMethodSignature);this.fastClassInfo = fci;this.createInfo = null;}}}}private static FastClass helper(CreateInfo ci, Class classType) {FastClass.Generator generator = new FastClass.Generator();generator.setType(classType);generator.setClassLoader(ci.proxyClass.getClassLoader());generator.setNamingPolicy(ci.namingPolicy);generator.setStrategy(ci.strategy);generator.setAttemptLoad(ci.attemptLoad);return generator.create();}private MethodProxy() {}public Signature getSignature() {return this.overrideMethodSignature;}public String getSuperName() {return this.cglibMethodSignature.getName();}public int getSuperIndex() {this.init();return this.fastClassInfo.cglibMethodIndex;}FastClass getFastClass() {this.init();return this.fastClassInfo.targetFastClass;}FastClass getSuperFastClass() {this.init();return this.fastClassInfo.proxyFastClass;}@SneakyThrowspublic static MethodProxy find(Class classType, Signature sig) {Method method = classType.getDeclaredMethod("findMethodProxy", MethodInterceptorGenerator.FIND_PROXY_TYPES);return (MethodProxy) method.invoke(null, sig);}private static class CreateInfo {Class targetClass;Class proxyClass;NamingPolicy namingPolicy;GeneratorStrategy strategy;boolean attemptLoad;public CreateInfo(Class targetClass, Class proxyClass) {this.targetClass = targetClass;this.proxyClass = proxyClass;AbstractClassGenerator fromEnhancer = AbstractClassGenerator.getCurrent();if (fromEnhancer != null) {this.namingPolicy = fromEnhancer.getNamingPolicy();this.strategy = fromEnhancer.getStrategy();this.attemptLoad = fromEnhancer.getAttemptLoad();}}}private static class FastClassInfo {FastClass targetFastClass;FastClass proxyFastClass;int overrideMethodIndex;int cglibMethodIndex;private FastClassInfo() {}}
}

FastClass

为了避免使用 Method 的 invoke 方法来进行反射调用而设计的。

TargetFastClass

public Object invoke(int methodIndex, Object obj, Object[] args) throws InvocationTargetException {// 这里无论传入的是ServiceImpl还是ServiceImplCglibProxy,都可以进行强转// 如果是ServiceImplCglibProxy,就调用同名方法ServiceImpl serviceImpl = (ServiceImpl)obj;// 没有匹配就抛出异常try {switch (methodIndex) {case 0:return serviceImpl.getMsg(((Number)args[0]).intValue(), ((Number)args[1]).intValue());case 1:// 如果传入的实际对象是ServiceImplCglibProxy类型的,那么就会调用增强方法,而增强方法又会进而到intercept中,从而造成死循环serviceImpl.show((String)args[0]);return null;}} catch (Throwable throwable) {throw new InvocationTargetException(throwable);}throw new IllegalArgumentException("Cannot find matching method/constructor");
}

ProxyFastClass

public Object invoke(int methodIndex, Object obj, Object[] args) throws InvocationTargetException {// 强转成代理对象(ServiceImplCglibProxy)ServiceImplCglibProxy serviceImplProxy = (ServiceImplCglibProxy)obj;// 没有匹配就抛出异常try {// ProxyFastClass的索引顺序和TargetFastClass的索引顺序没有任何关系switch (methodIndex) {case 0:return serviceImpl.getMsg(((Number)args[0]).intValue(), ((Number)args[1]).intValue());case 1:return serviceImpl.cglibGetMsg(((Number)args[0]).intValue(), ((Number)args[1]).intValue());case 2:serviceImpl.cglibShow((String)args[0]);return null;case 3:serviceImpl.show((String)args[0]);return null;}} catch (Throwable throwable) {throw new InvocationTargetException(throwable);}throw new IllegalArgumentException("Cannot find matching method/constructor");
}

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

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

相关文章

麒麟系统使用桌面共享远程桌面

客户端安装vinager 服务端 安装 vnc4server xrdp tightvncserver vino 安装完成后 需要重启 在用户的家目录下新建 .xsession 写入xfce4-session防止闪退 雪花屏 开启xrdp服务 远程链接 Vnc只能链接系统登录的用户 Rdp可以链接所有普通用户

【C语言】结构体内存对齐

目录 引入结构体 结构的声明 创建和初始化 内部元素的使用&#xff1b; 特殊声明&#xff1a; 结构体在内存中的对齐 练习&#xff1a; 引入结构体 C语言有各种数据类型&#xff0c;我们已经对一些数据类型很熟悉&#xff1a; 整型&#xff08;int&#xff09;- 存储整…

京东商品详情数据在数据分析行业中的重要性

京东商品详情数据在数据分析行业中具有重要作用。这些数据提供了丰富的信息&#xff0c;可以帮助企业了解市场趋势、消费者需求、产品表现以及运营策略等多个方面。 首先&#xff0c;京东商品详情数据可以为企业提供市场趋势分析的依据。通过观察商品的销售量、销售额、价格等…

c语言:理解和避免野指针

野指针的定义&#xff1a; 野指针是指一个指针变量存储了一个无效的地址&#xff0c;通常是一个未初始化的指针或者指向已经被释放的内存地址。当程序尝试使用野指针时&#xff0c;可能会导致程序崩溃、内存泄漏或者其他不可预测的行为。因此&#xff0c;在编程中需要特别注意…

Pandas中DataFrame对象的创建与常用属性方法(第2讲)

Pandas中DataFrame对象的创建与常用属性方法(第2讲)         🍹博主 侯小啾 感谢您的支持与信赖。☀️ 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔…

智能优化算法应用:基于孔雀算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于孔雀算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于孔雀算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.孔雀算法4.实验参数设定5.算法结果6.参考文献7.MATLAB…

[足式机器人]Part2 Dr. CAN学习笔记-数学基础Ch0-2 特征值与特征向量

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记-数学基础Ch0-2 特征值与特征向量 1. 定义1.1 线性变换1.2 求解特征值&#xff0c;特征向量1.3 应用&#xff1a;对角化矩阵——解耦Decouple 2. Summary 1. 定义 A v ⃗ λ v ⃗ A\vec{v}\lambd…

【网络奇缘】- 计算机网络|深入学习物理层|网络安全

​ &#x1f308;个人主页: Aileen_0v0&#x1f525;系列专栏: 一见倾心,再见倾城 --- 计算机网络~&#x1f4ab;个人格言:"没有罗马,那就自己创造罗马~" 回顾链接&#xff1a;http://t.csdnimg.cn/ZvPOS 这篇文章是关于深入学习原理参考模型-物理层的相关知识点&…

Linux权限命令详解

Linux权限命令详解 文章目录 Linux权限命令详解一、什么是权限&#xff1f;二、权限的本质三、Linux中的用户四、linux中文件的权限4.1 文件访问者的分类&#xff08;人&#xff09;4.2 文件类型和访问权限&#xff08;事物属性&#xff09; 五、快速掌握修改权限的做法【第一种…

实战1-python爬取安全客新闻

一般步骤&#xff1a;确定网站--搭建关系--发送请求--接受响应--筛选数据--保存本地 1.拿到网站首先要查看我们要爬取的目录是否被允许 一般网站都会议/robots.txt目录&#xff0c;告诉你哪些地址可爬&#xff0c;哪些不可爬&#xff0c;以安全客为例子 2. 首先测试在不登录的…

Docker Network(网络)——8

目录&#xff1a; Docker 为什么需要网络管理Docker 网络架构简介 CNMLibnetwork驱动常见网络类型 bridge 网络host 网络container 网络none 网络overlay 网络docker 网络管理命令 docker network createdocker network inspectdocker network connectdocker network disconne…

class072 最长递增子序列问题与扩展【算法】

class072 最长递增子序列问题与扩展【算法】 code1 300. 最长递增子序列 // 最长递增子序列和最长不下降子序列 // 给定一个整数数组nums // 找到其中最长严格递增子序列长度、最长不下降子序列长度 // 测试链接 : https://leetcode.cn/problems/longest-increasing-subsequen…

你知道MySQL中 group by 怎么优化吗

更好的阅读体验&#xff0c;请点击 YinKai s Blog。 ​ 在 MySQL 中 group by 用于按照一个或多个列对结果集进行分组。在讨论 group by 怎么优化之前&#xff0c;我们先来看看 group by 的执行流程&#xff0c;这样我们才能对症下药。 group by 执行流程 ​ 我们先用下面的 …

Ubuntu 18.04使用Qemu和GDB搭建运行内核的环境

安装busybox 参考博客&#xff1a; 使用GDBQEMU调试Linux内核环境搭建 一文教你如何使用GDBQemu调试Linux内核 ubuntu22.04搭建qemu环境测试内核 交叉编译busybox 编译busybox出现Library m is needed, can’t exclude it (yet)的解释 S3C2440 制作最新busybox文件系统 https:…

2024年网络安全竞赛-Web安全应用

Web安全应用 (一)拓扑图 任务环境说明: 1.获取PHP的版本号作为Flag值提交;(例如:5.2.14) 2.获取MySQL数据库的版本号作为Flag值提交;(例如:5.0.22) 3.获取系统的内核版本号作为Flag值提交;(例如:2.6.18) 4.获取网站后台管理员admin用户的密码作为Flag值提交…

udp多播组播

import socket ,struct,time# 组播地址和端口号 MCAST_GRP 239.0.0.1 MCAST_PORT 8888 # 创建UDP socket对象 sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # 绑定socket对象到本地端口号 # sock.bind((MCAST_GRP, MCAST_PORT)) …

【4】PyQt输入框

1. 单行文本输入框 QLineEdit控件可以输入单行文本 from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QVBoxLayout from PyQt5.QtCore import * from PyQt5.QtGui import QIcon import sysdef init_widget(w: QWidget):# 修改窗口标题w.setWindowTitle(单行输…

前端面试——CSS面经(持续更新)

1. CSS选择器及其优先级 !important > 行内样式 > id选择器 > 类/伪类/属性选择器 > 标签/伪元素选择器 > 子/后台选择器 > *通配符 2. 重排和重绘是什么&#xff1f;浏览器的渲染机制是什么&#xff1f; 重排(回流)&#xff1a;当增加或删除dom节点&…

【面试经典150 | 二叉树】从中序与后序遍历序列构造二叉树

文章目录 写在前面Tag题目来源题目解读解题思路方法一&#xff1a;递归 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对于本题涉及到的数据结构等内容…