深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?

本文主要是介绍深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

要说双亲委派机制,还得从类加载器的类型谈起

一、类加载器的类型

类加载器有以下种类:

  • 启动类加载器(Bootstrap ClassLoader)
  • 扩展类加载器(Extension ClassLoader)
  • 应用类加载器(Application ClassLoader)

启动类加载器

内嵌在JVM内核中的加载器,由C++语言编写(因此也不会继承ClassLoader),是类加载器层次中最顶层的加载器。用于加载java的核心类库,即加载jre/lib/rt.jar里所有的class。由于启动类加载器涉及到虚拟机本地实现细节,我们无法获取启动类加载器的引用。

扩展类加载器

它负责加载JRE的扩展目录,jre/lib/ext或者由java.ext.dirs系统属性指定的目录中jar包的类。父类加载器为启动类加载器,但使用扩展类加载器调用getParent依然为null。

应用类加载器

又称系统类加载器,可用通过 java.lang.ClassLoader.getSystemClassLoader()方法获得此类加载器的实例,系统类加载器也因此得名。应用类加载器主要加载classpath下的class,即用户自己编写的应用编译得来的class,调用getParent返回扩展类加载器。

扩展类加载器与应用类加载器继承结构如图所示:

可以看到除了启动类加载器,其余的两个类加载器都继承于ClassLoader,我们自定义的类加载器,也需要继承ClassLoader。

值得注意的是,启动类、扩展类与应用类加载器之间的父子关系,并不是通过继承来实现的,而是通过组合,即使用parent变量来保存“父加载器”的引用。


二、双亲委派机制

当一个类加载器收到了一个类加载请求时,它自己不会先去尝试加载这个类,而是把这个请求转交给父类加载器,每一个层的类加载器都是如此,因此所有的类加载请求都应该传递到最顶层的启动类加载器中。只有当父类加载器在自己的加载范围内没有搜寻到该类时,并向子类反馈自己无法加载后,子类加载器才会尝试自己去加载。

加载标准类库与用户代码,会有不同的方式:

 ClassLoader内的loadClass方法,就很好的解释了双亲委派的加载过程:

    protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {//检查该class是否已经被当前类加载器加载过Class<?> c = findLoadedClass(name);if (c == null) {//此时该class还没有被加载try {if (parent != null) {//如果父加载器不为null,则委托给父类加载c = parent.loadClass(name, false);} else {//如果父加载器为null,说明当前类加载器已经是启动类加载器,直接时候用启动类加载器去加载该classc = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}if (c == null) {//此时父类加载器都无法加载该class,则使用当前类加载器进行加载long t1 = System.nanoTime();c = findClass(name);...}}//是否需要连接该类if (resolve) {resolveClass(c);}return c;}}

三、双亲委派存在的意义

为什么要使用双亲委派机制呢?

假设用户自己定义了java.lang.Object类,由于双亲委派机制的存在,最终会委托到启动类加载器去加载,即返回rt.jar中的Object类,并不会加载用户编写的Object类。

大家上班摸鱼刷的LeetCode,本质上自定义了一个类加载器,重写了findClass方法,会从网络中加载字节码,生成Class对象,最终通过loadClass定义的双亲委派机制进行加载。如果这个时候,我定义了一个恶意java.lang.Object类,在没有双亲委派机制的情况下,可能会对jvm产生安全风险。

双亲委派机制存在的意义,就是为了防止findClass与defineclass生成的Class对象覆盖掉标准类库中的基础类,避免产生安全风险。


四、如何自定义类加载器

我们整理ClassLoader里面的流程

  1. loadclass:双亲委派机制,子加载器委托父加载器加载,父加载器都加载失败时,子加载器通过findclass自行加载
  2. findclass:当前类加载器根据路径以及class文件名称加载字节码,从class文件中读取字节数组,然后使用defineClass
  3. defineclass:根据字节数组,返回Class对象

我们在ClassLoader里面找到findClass方法,发现该方法直接抛出异常,应该是留给子类实现的。

    protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);}

到这里,我们应该明白,loadClass方法使用了模版方法模式,主线逻辑是双亲委派,但如何将class文件转化为Class对象的步骤,已经交由子类去实现。对模版方法模式不熟悉的同学,可以先参考我的另外一篇文章模版方法模式

其实源码中,已经有一个自定义类加载的样例代码,在注释中:

      class NetworkClassLoader extends ClassLoader {String host;int port;public Class findClass(String name) {byte[] b = loadClassData(name);return defineClass(name, b, 0, b.length);}private byte[] loadClassData(String name) {// load the class data from the connection}}

看得出来,如果我们需要自定义类加载器,只需要继承ClassLoader,并且重写findClass方法即可。

现在有一个简单的样例,class文件依然在文件目录中:

package com.yang.testClassLoader;import sun.misc.Launcher;import java.io.*;public class MyClassLoader extends ClassLoader {/*** 类加载路径,不包含文件名*/private String path;public MyClassLoader(String path) {super();this.path = path;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] bytes = getBytesFromClass(name);assert bytes != null;//读取字节数组,转化为Class对象return defineClass(name, bytes, 0, bytes.length);}//读取class文件,转化为字节数组private byte[] getBytesFromClass(String name) {String absolutePath = path + "/" + name + ".class";FileInputStream fis = null;ByteArrayOutputStream bos = null;try {fis = new FileInputStream(new File(absolutePath));bos = new ByteArrayOutputStream();byte[] temp = new byte[1024];int len;while ((len = fis.read(temp)) != -1) {bos.write(temp, 0, len);}return bos.toByteArray();} catch (IOException e) {e.printStackTrace();} finally {if (null != fis) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}if (null != bos) {try {bos.close();} catch (IOException e) {e.printStackTrace();}}}return null;}public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {MyClassLoader classLoader = new MyClassLoader("C://develop");Class test = classLoader.loadClass("Student");test.newInstance();}
}

Student类:

public class Student {public Student() {System.out.println("student classloader is" + this.getClass().getClassLoader().toString());}
}

注意,这个Student类千万不要加包名,idea报错不管他即可,然后使用javac Student.java编译该类,将生成的class文件复制到c://develop下即可。

运行MyClassLoader的main方法后,可以看到输出:

看得出来,Student.class确实是被我们自定义的类加载器给加载了。


五、双亲委派机制能被破坏吗

从上面的自定义类加载器的内容中,我们应该可以猜到了,破坏双亲委派直接重写loadClass方法就完事了。事实上,我们确实可以重写loadClass方法,毕竟这个方法没有被final修饰。双亲委派既然有好处,为什么jdk对loadClass开放重写呢?这要从双亲委派引入的时间来看:

双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,jdk为了向前兼容,不得已开放对loadClass的重写操作。

当然,破坏也不止这一次,jdbc与tomcat也破坏了双亲委派。


六、JDBC对双亲委派的破坏

还记得,我们第一次学jdbc的时候,是怎么连接数据库的吗?

先引用一个mysql-connector-java的jar包,这里的版本是5.0.8

        <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.0.8</version></dependency>
        Class.forName("com.mysql.jdbc.Driver");Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");

这段代码,真的是勾起了我好多回忆啊~   想起了那年在夕阳下奔跑的时光,那是我逝去的青春

首先要说明的是,该版本的jdbc并没有去打破双亲委派,或者说jdbc4.0前没有破坏双亲委派

数据库这么多,jdk为了统一管理数据库驱动,在java.sql下定义了Driver接口,具体的实现由数据库厂商去做。

mysql对Driver接口的实现类是com.mysql.jdbc.Driver类,位于我们新引入的jar包中。

我们进入Class.forName中,发现最终会使用应用类加载器去加载com.mysql.jdbc.Driver类。

而该Driver位于引入的jar包中,确实是应该被应用类加载器加载。

 接着进入到com.mysql.jdbc包下的Driver类中,它实现了rt.jar中的java.sql.Driver接口。

 Class.forName会初始化该类,初始化的时候会执行静态方法。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {public Driver() throws SQLException {}static {try {//将mysql的Driver注册进驱动管理器中DriverManager.registerDriver(new Driver());} catch (SQLException var1) {throw new RuntimeException("Can't register driver!");}}
}

所以,整个过程是:

  1. Class.forName会使用应用类加载器加载Driver实现类
  2. 加载Driver实现类需要执行静态方法,即将mysql的Driver注册进驱动管理器中,那么此时需要加载DriverManager类
  3. 应用类加载器去加载DriverManager类,而DriverManager位于rt.jar中,便一直向上委托到启动类加载器完成加载

这个过程确实没有破坏双亲委派

那么jdbc4.0后的情况呢?

为了使用该特性,我们需要引入高版本的mysql-connector-java,这里引入的版本是5.1.8

此时完全可以抛弃第一行的Class.forName语句了,使用以下语句来进行实验

        Enumeration<Driver> en = DriverManager.getDrivers();while (en.hasMoreElements()) {java.sql.Driver driver = en.nextElement();System.out.println(driver);}

输出为:

 看来内存中已经存在mysql的Driver了,这到底是怎么做的呢?

应用类加载器逐层委托到启动类加载器去加载DriverManager时,会同时执行它的静态方法

    static {loadInitialDrivers();println("JDBC DriverManager initialized");}

loadInitialDrivers内部核心的代码这有这两句

    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();

看到ServiceLoader,大家想到了什么,这不是jdk spi机制的核心吗?

spi机制在我的这篇文章SpringBoot的自动装配原理、自定义starter与spi机制,一网打尽有详细的一个介绍,并且对比了SpringBoot与JDK中spi机制的异同。

既然使用到了spi机制,那么mysql-connector-java的jar包在META-INF目录下必然有services目录,内容如下。

 启动类加载DriverManager,之后需要通过spi机制去加载jar包中的Driver类,而该Driver理应被应用类加载器加载,这个时候就需要启动类加载器去通知应用类加载器,这明显违背了双亲委派机制

那么,启动类加载器是怎么去通知应用类加载器的呢?

我们继续进入到ServiceLoader.load方法中

    public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);}

Thread.currentThread().getContextClassLoader()是线程上下文类加载器,看来最终使用的是线程上下文类加载器去加载的Driver实现类。

而在sun.misc.Launcher类中,将应用类加载器设置进了线程上下文类加载器中,所以可以理解为,通过线程上下文类加载器,我们可以拿到应用类加载器的引用。

    public Launcher() {this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);Thread.currentThread().setContextClassLoader(this.loader);}

在jdbc4.0的情况下,梳理一下整个过程:

  1. 应用类加载器逐层委托到启动类加载器去加载DriverManager类
  2. 启动类加载器加载DriverManager类时,会执行其静态方法,即通过spi机制去加载jar包中的Driver实现类
  3. 此时启动类加载器需要委托应用类加载器加载Driver实现类,具体做法是通过线程上下文类加载器拿到应用类加载器的引用

确实是破坏了双亲委派!


七、Tomcat对双亲委派的破坏

tomcat有两个最基础的知识点,一个是应用打包放在webapps目录下就可以运行,另外一个是修改jsp会实时生效。

那这里抛出几个问题,来猜想一下Tomcat中类加载器的一个结构。

(1)jsp实时生效是怎么做的?

首先,在jvm中,如何去确定类的唯一性呢?是由类加载器实例+全限定名一起确定的。全限定名相同,类加载器不同,则会被认定为不同的类。

jsp文件被修改后,会被重新编译成Servlet,全限定名肯定是不变的,如果这个时候不去卸载加载该Servlet的类加载器,那么新jsp是无论如何都不会被加载进来的。因此,我们可以得知,每一个jsp文件都会对应一个类加载器实例

(2)每个webapps下的应用依赖的类库是否会互相影响?

显然是不会影响的。应用A依赖低版本的Spring,而应用B依赖高版本的Spring,都是允许的。虽然Spring的版本不同,但某些类的全限定名是完全一致的。如果应用A与应用B采用同一个类加载器,是不会允许Spring版本不一样的。这里,我们猜想webapps下的每一个应用都会对应一个不同的类加载器实例,用以保持应用间的隔离。

从以上的两个问题,我们可以了解到:每一个jsp(或者说servlet)都对应一个不同的类加载器实例,每个webapp应用也是。

其实,tomcat5版本(以下如果没有另外声明版本,那么都是以该版本为例)的类加载器结构为:

 其中各个加载器加载的范围为:

  • Common ClassLoader:主要加载common目录下的资源
  • Catalina ClassLoader:主要加载server目录下的资源
  • Shared ClassLoader:主要加载shared目录下的资源
  • Webapp ClassLoader:每一个应用会对应与该类型的一个实例,主要加载该应用下的WEB-INF下的资源
  • JasperLoader:每一个jsp文件会对应于该类型的一个实例,就是为了修改jsp能及时生效

(前三个类加载器在tomcat6中已经合并了,合并之后的加载器加载lib目录下的资源)

它们在Bootstrap类中有过声明:

    protected ClassLoader commonLoader = null;protected ClassLoader catalinaLoader = null;protected ClassLoader sharedLoader = null;private void initClassLoaders() {try {commonLoader = createClassLoader("common", null);if( commonLoader == null ) {// no config file, default to this loader - we might be in a 'single' env.commonLoader=this.getClass().getClassLoader();}catalinaLoader = createClassLoader("server", commonLoader);sharedLoader = createClassLoader("shared", commonLoader);} catch (Throwable t) {log.error("Class loader creation threw exception", t);System.exit(1);}}

其中createClassLoader方法会从container\catalina\src\conf\catalina.properties配置中读取每个加载器加载的范围:

common.loader=${catalina.home}/common/classes,${catalina.home}/common/i18n/*.jar,${catalina.home}/common/endorsed/*.jar,${catalina.home}/common/lib/*.jarserver.loader=${catalina.home}/server/classes,${catalina.home}/server/lib/*.jarshared.loader=${catalina.base}/shared/classes,${catalina.base}/shared/lib/*.jar

catalina.home是安装目录,catalina.base是每个tomcat实例的工作目录。在只用一个tomcat的情况下,两个目录是一样的。

在了解了加载器的类型与范围之后,那么tomcat到底是怎么打破双亲委派机制的呢?

前面说过,双亲委派机制被定义在ClassLoader中的loadClass方法中,如果某个自定义的类加载想要打破双亲委派,那么重新loadClass方法即可。

Tomcat中的WebappClassLoader就是自定义类加载器,它的loadClass方法为:
 

    public Class loadClass(String name) throws ClassNotFoundException {return (loadClass(name, false));}public Class loadClass(String name, boolean resolve)throws ClassNotFoundException {if (log.isDebugEnabled())log.debug("loadClass(" + name + ", " + resolve + ")");Class clazz = null;// Log access to stopped classloaderif (!started) {try {throw new IllegalStateException();} catch (IllegalStateException e) {log.info(sm.getString("webappClassLoader.stopped", name), e);}}//1、从自己的本地缓存中查找,本地缓存的数据结构为ResourceEntryclazz = findLoadedClass0(name);if (clazz != null) {if (log.isDebugEnabled())log.debug("  Returning class from cache");if (resolve)resolveClass(clazz);return (clazz);}//2、从jvm的缓存中查找clazz = findLoadedClass(name);if (clazz != null) {if (log.isDebugEnabled())log.debug("  Returning class from cache");if (resolve)resolveClass(clazz);return (clazz);}//3、如果缓存中都找不到,则利用系统类加载器加载try {clazz = system.loadClass(name);if (clazz != null) {if (resolve)resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {// Ignore}if (securityManager != null) {int i = name.lastIndexOf('.');if (i >= 0) {try {securityManager.checkPackageAccess(name.substring(0,i));} catch (SecurityException se) {String error = "Security Violation, attempt to use " +"Restricted Class: " + name;log.info(error, se);throw new ClassNotFoundException(error, se);}}}boolean delegateLoad = delegate || filter(name);//4、开启代理的话,则使用父加载器加载if (delegateLoad) {if (log.isDebugEnabled())log.debug("  Delegating to parent classloader1 " + parent);ClassLoader loader = parent;if (loader == null)loader = system;try {clazz = loader.loadClass(name);if (clazz != null) {if (log.isDebugEnabled())log.debug("  Loading class from parent");if (resolve)resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {;}}//5、自行加载if (log.isDebugEnabled())log.debug("  Searching local repositories");try {clazz = findClass(name);if (clazz != null) {if (log.isDebugEnabled())log.debug("  Loading class from local repository");if (resolve)resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {;}//如果自己也加载不了,那就只能让父加载器加载了if (!delegateLoad) {if (log.isDebugEnabled())log.debug("  Delegating to parent classloader at end: " + parent);ClassLoader loader = parent;if (loader == null)loader = system;try {clazz = loader.loadClass(name);if (clazz != null) {if (log.isDebugEnabled())log.debug("  Loading class from parent");if (resolve)resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {;}}throw new ClassNotFoundException(name);}

loadClass内部的逻辑整理如下:

  1. 先从WebappClassLoader的ResourceEntry缓存中查找
  2. 从jvm缓存中查找,比如去元数据区查找
  3. 利用系统类(应用类)加载器加载,避免webapp中的类覆盖掉标准类库中的类。
  4. 开启代理的话,则使用父加载器加载,这个默认没开启的。
  5. webappClassLoader自行去加载
  6. 自己也没加载成功的话,最后只能让父加载器去加载

这里有一个问题,对于一些非基础类库,为什么要先让webappClassLoader先去加载呢?

假设应用a依赖1.0版本的x.jar,而应用b依赖2.0版本的x.jar。为了保证两个应用的隔离性,首先要做的就是保证两个应用各自对应不同的webappClassLoader实例。如果这两个webappClassLoader实例在加载x.jar的时候,直接向上委托,那么最终只会加载一个版本的x.jar。

从上面,我们可以了解到:

对于一些标准类库中的类,比如Object类,会让系统类加载器加载,然后一直委托到启动类加载器,这个过程是没有违背双亲委派的

而对于webapp中独有的类,则是webappClassLoader自行去加载,加载失败才让父加载器加载,明显是违背双亲委派的。


八、总结

双亲委派机制,核心是子加载器委托父加载器,能够避免java核心类库被篡改,增加了安全性。

但发展会带来创新,创新就会带来变革,jdbc与tomcat打破了这个自古相传的机制。

在jdbc中,父加载器委托子加载器。即利用线程上下文类加载器,让启动类加载器得以委托应用类加载器,去加载jar中的数据库驱动。

在tomcat中,子加载器优先于父加载器加载。即为了实现各个webapp的隔离性,webappClassLoader会先于父加载器加载。

这篇关于深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/831485

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

AI绘图怎么变现?想做点副业的小白必看!

在科技飞速发展的今天,AI绘图作为一种新兴技术,不仅改变了艺术创作的方式,也为创作者提供了多种变现途径。本文将详细探讨几种常见的AI绘图变现方式,帮助创作者更好地利用这一技术实现经济收益。 更多实操教程和AI绘画工具,可以扫描下方,免费获取 定制服务:个性化的创意商机 个性化定制 AI绘图技术能够根据用户需求生成个性化的头像、壁纸、插画等作品。例如,姓氏头像在电商平台上非常受欢迎,

W外链微信推广短连接怎么做?

制作微信推广链接的难点分析 一、内容创作难度 制作微信推广链接时,首先需要创作有吸引力的内容。这不仅要求内容本身有趣、有价值,还要能够激起人们的分享欲望。对于许多企业和个人来说,尤其是那些缺乏创意和写作能力的人来说,这是制作微信推广链接的一大难点。 二、精准定位难度 微信用户群体庞大,不同用户的需求和兴趣各异。因此,制作推广链接时需要精准定位目标受众,以便更有效地吸引他们点击并分享链接

电脑桌面文件删除了怎么找回来?别急,快速恢复攻略在此

在日常使用电脑的过程中,我们经常会遇到这样的情况:一不小心,桌面上的某个重要文件被删除了。这时,大多数人可能会感到惊慌失措,不知所措。 其实,不必过于担心,因为有很多方法可以帮助我们找回被删除的桌面文件。下面,就让我们一起来了解一下这些恢复桌面文件的方法吧。 一、使用撤销操作 如果我们刚刚删除了桌面上的文件,并且还没有进行其他操作,那么可以尝试使用撤销操作来恢复文件。在键盘上同时按下“C

webm怎么转换成mp4?这几种方法超多人在用!

webm怎么转换成mp4?WebM作为一种新兴的视频编码格式,近年来逐渐进入大众视野,其背后承载着诸多优势,但同时也伴随着不容忽视的局限性,首要挑战在于其兼容性边界,尽管WebM已广泛适应于众多网站与软件平台,但在特定应用环境或老旧设备上,其兼容难题依旧凸显,为用户体验带来不便,再者,WebM格式的非普适性也体现在编辑流程上,由于它并非行业内的通用标准,编辑过程中可能会遭遇格式不兼容的障碍,导致操

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

怎么让1台电脑共享给7人同时流畅设计

在当今的创意设计与数字内容生产领域,图形工作站以其强大的计算能力、专业的图形处理能力和稳定的系统性能,成为了众多设计师、动画师、视频编辑师等创意工作者的必备工具。 设计团队面临资源有限,比如只有一台高性能电脑时,如何高效地让七人同时流畅地进行设计工作,便成为了一个亟待解决的问题。 一、硬件升级与配置 1.高性能处理器(CPU):选择多核、高线程的处理器,例如Intel的至强系列或AMD的Ry

滚雪球学Java(87):Java事务处理:JDBC的ACID属性与实战技巧!真有两下子!

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE啦,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~ 🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!! 环境说明:Windows 10

基于UE5和ROS2的激光雷达+深度RGBD相机小车的仿真指南(五):Blender锥桶建模

前言 本系列教程旨在使用UE5配置一个具备激光雷达+深度摄像机的仿真小车,并使用通过跨平台的方式进行ROS2和UE5仿真的通讯,达到小车自主导航的目的。本教程默认有ROS2导航及其gazebo仿真相关方面基础,Nav2相关的学习教程可以参考本人的其他博客Nav2代价地图实现和原理–Nav2源码解读之CostMap2D(上)-CSDN博客往期教程: 第一期:基于UE5和ROS2的激光雷达+深度RG

韦季李输入法_输入法和鼠标的深度融合

在数字化输入的新纪元,传统键盘输入方式正悄然进化。以往,面对实体键盘,我们常需目光游离于屏幕与键盘之间,以确认指尖下的精准位置。而屏幕键盘虽直观可见,却常因占据屏幕空间,迫使我们在操作与视野间做出妥协,频繁调整布局以兼顾输入与界面浏览。 幸而,韦季李输入法的横空出世,彻底颠覆了这一现状。它不仅对输入界面进行了革命性的重构,更巧妙地将鼠标这一传统外设融入其中,开创了一种前所未有的交互体验。 想象