【Spring专题】Spring底层核心原理解析

目录

  • 前言
  • 阅读导航
  • 前置知识
    • Q1:你能描述一下JVM对象创建过程吗?
    • Q2:Spring的特性是什么?
    • 前置知识总结
  • 课程内容
    • 一、Spring容器的启动
    • 二、一般流程推测
      • 2.1 扫描
      • 2.2 IOC
      • 2.3 AOP
    • 2.4 小结
    • 三、【扫描】过程简单推测
    • 四、【IOC】过程简单推测
      • 4.1 推断构造方法过程细讲
    • 五、【AOP】过程简单推测
    • 六、Spring事务
  • 学习总结

前言

Spring啊,可以说是我们大部分Java玩家【最熟悉的陌生人】了吧。八个字形容:似懂非懂,会也不会
在这里插入图片描述
你说简单应用,我们大家都会,那真要展开说两句的话,那只能来这么两句:这是第一句,接着是第二句,好了我说完了。
在这里插入图片描述
但是啊xdm,据说Spring是一份非常非常非常优秀的源码,不但有丰富的设计模式应用场景,代码写的也很优美,有条理,所以非常推荐大家学习。除了能在日常装逼以外,还能丰富一下见识,提升自己写代码的能力。

阅读导航

阅读对象:有过Spring开发经验的人

前置知识

Q1:你能描述一下JVM对象创建过程吗?

答:看图说话:
在这里插入图片描述

  1. 类加载:在使用一个类之前,Java虚拟机需要先将类的字节码加载到内存中。类加载是Java虚拟机的核心过程,它负责查找类的字节码文件并加载到内存的方法区。类加载包括加载、验证、准备、解析和初始化这五个阶段。
  2. 分配内存:在类加载完成后,Java虚拟机会为对象分配内存空间。内存分配通常在堆(Heap)上进行,但也有一些特殊情况下的对象可以在栈(Stack)上分配内存,例如线程栈上的局部对象。
  3. 实例化(初始化零值):在分配内存后,Java虚拟机会将对象的内存空间初始化为零值。这包括基本类型的默认值(例如0、false等)和引用类型的默认值(null)。
  4. 设置对象头:Java对象在内存中的布局包括对象头和实例数据两部分。对象头存储了一些元数据,如对象的哈希码、锁状态等。在对象创建过程中,Java虚拟机会设置对象头的值。
  5. 执行构造方法:对象创建的最后一步是执行构造方法。构造方法用于初始化对象的实例数据,并执行其他必要的初始化操作。构造方法可以是类的默认构造方法,也可以是自定义的构造方法。
  6. 返回对象引用:对象创建完成后,Java虚拟机会返回一个指向该对象的引用。通过引用,程序可以操作对象的属性和方法。

PS:为什么要问这个问题?因为Spring是IOC技术,就算再怎么玩出花来,他也要按照这个基本流程来创建对象。只不过,可以提前告知大家的是,SpringIOC在这个流程之中,穿插了很多自己的逻辑,以此丰富了IOC的功能!

Q2:Spring的特性是什么?

答:Spring的特性就是IOC跟AOP两大概念!甚至可以这么说:Spring就是实现了AOP技术的IOC容器(容器,容器,容器)

Q3:什么是IOC,什么是AOP?
答:下面答案来源于百度【文心一言】:

  • IOC(控制反转)是一种设计模式(思想),它允许将对象的创建和管理交给Spring容器来处理,而不是在代码中直接创建对象。通过使用IOC,可以将对象的依赖关系从代码中解耦,使得代码更加灵活、可维护和可测试。
  • AOP(面向切面编程)也是一种设计模式(思想),它通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态添加功能。AOP解决了面向对象编程中无法解决的问题,例如事务管理、安全性、日志记录等。

Spring框架通过实现IOC和AOP,使得程序更加模块化、灵活和易于维护。同时,Spring还提供了许多其他模块和功能,如DAO、ORM、WebMVC等,使得它成为一个功能强大的Java开发框架。

前置知识总结

从上面的问题里面,我们提到了一个很重要的东西,即:Spring就是实现了AOP技术的IOC容器。并且,也概括地描述了IOC跟AOP的概念。既然我们也知道了,IOC其实也管理了对象的创建,那么说到对象创建,肯定也离不开我们在Q1说的,对象创建的过程。而且,无论对象怎么创建,谁创建,都没办法离开上面的流程的。
事实上,可以提前告诉大家的是,IOC在创建对象的过程,无非就是在上面的对象创建流程中,丰富了一些细节,新增了一些拓展点,为Spring功能实现提供支持。

课程内容

为了展开对Spring源码的研究,我们这里线大致地串讲一下Spring的一些核心知识点,让大家对Spring的底层一些基础逻辑有个清晰的认知。

一、Spring容器的启动

我想,经历过SSM/SSH时代的朋友,对下面的代码都不会陌生:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = (UserService) context.getBean("userService");
userService.test();
System.out.println(userService);
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-4.3.xsd"><!--    <import resource="引入其他bean xml配置文件" />--><bean id="userService" class="org.example.spring.bean.UserService"/>
</beans>

如果真的很陌生也没关系,下面这个可能就相对熟悉一点了:(后面也会围绕这个启动方式的Spring讲解。除了是下面的比较主流,也因为,下面这种方式使用更广、更新,内容相对丰富点!

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = (UserService) context.getBean("userService");
userService.test();
System.out.println(userService);
@Component
public class UserService {public void test() {System.out.println("这是一个测试方法");}
}

哈哈,我估计很多直接进入了Java【SpringBoot】时代的朋友,可能连上面这个都没看到过。
那上面段代码是干啥的呢?很简单,就是启动一个Spring容器而已。上面两个不同的启动方式,也仅仅是Bean注册方式不一样。比如前者是通过读取xml里面的<bean>标签定义,后者是读取的注解式Bean。
到这里,想问大家一个问题,那就是,通过上面第二种方式的代码,你发现了什么?我的发现是:我仅仅只是调用了一行代码,就可以开始使用Spring定义的Bean了,什么依赖注入,AOP啥的,我都没管,直接就可以了。这证明了啥?其实很粗浅,也有点废话,那就是证明:通过这一行代码,里面就帮我完成了所有我们平时使用过的,Spring的基础能力

二、一般流程推测

根据我们之前学习过Spring相关的操作,简单推测一下,这一行代码里面干了什么。

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

2.1 扫描

首先是第一点【扫描】。我们在项目中,写了这么多Bean,或者说,我项目里面这么多类,哪些是Bean,哪些是普通类,Spring是怎么识别到的?其实道理很简单的,Spring它也没那么智能,想要获取这个类的信息,Spring肯定要【亲自】去看一眼,才能知道这个类的具体信息。你有这么多少个文件,他就要扫描多少个类。关键代码如下:

// 定义需要扫描的基础包名
@ComponentScan("org.tuling.spring")
public class AppConfig {
}

2.2 IOC

扫描完了所有的文件,那基本上Spring已经能确定,哪些是Bean,哪些是普通类了。接下来,就可以开始创建Bean了,这里,就是所谓的IOC过程

2.3 AOP

AOP肯定是发生在IOC之后的,如果你们了解设计模式里面的【代理模式】的话,理解这一点并不难。毕竟,如果目标对象功能不完整,代理对象的功能也会收到影响。

2.4 小结

在这里插入图片描述

三、【扫描】过程简单推测

之前我们也说了,扫描,就是需要Spring亲自去看一看,哪些是需要被创建Bean的,哪些是不需要的。就拿我们举例用的new AnnotationConfigApplicationContext(AppConfig.class)来说,大概步骤如下:(简单推测,不详)

  1. 它需要先看AppConfig.class,读取扫描包的基础路径
  2. 根据上一步读到的基础路径,遍历包下所有的文件,如果类上存在@Component@Service等注解,则确认为是一个Bean
  3. 筛选完后将读取的Bean信息记录下来,比如说存到一个Map里面,方便后续遍历

在这里插入图片描述

四、【IOC】过程简单推测

IOC过程,其实在Spring中有个比较专业的术语,叫做:Bean的生命周期。简单的几个字,包含了很多内容。在此之前,大家先看一看【前置知识】里面【JVM对象创建过程】,加深一下印象。
其中有:

  1. 新建一个Bean对象。说到新建Bean对象,无论如何都绕不开构造方法的调用。我有多个构造函数,那我应该使用哪个呢?这在Spring内部,叫做:【推断构造方法】;
  2. Bean对象创建好了之后,当然不能忘了我们的:【依赖注入】;
  3. 有一点大家可能用的会比较少,但如果有过经验的同学可能会比较熟悉,比如:ApplicationContextAware,我们可以直接直译,叫做:ApplicationContext感知,感知ApplicationContext,所以通过设置,我们就可以在Bean里面获取到这个组件了;
  4. init方法。如果大家有过@PostConstruct以及InitializingBean使用经验的话,或许知道,创建完Bean之后,在返回Bean之前,还有这一步动作。事实上,根据我们在前言里面说的【IOC在创建对象的过程,无非就是在上面的对象创建流程中,丰富了一些细节,新增了一些拓展点】,所以,在IOC里面还有很多切入点,比如:实例化前、实例化后;初始化前、初始化后等等
  5. 最后,就是进入AOP的流程了。如果需要AOP,则进行AOP,并且返回AOP对象;否则直接返回已经创建好的对象

另外需要注意的是,Bean对象创建出来后:

  1. 如果当前Bean是单例Bean,那么会把该Bean对象存入一个Map<String, Object>,Map的key为beanName,value为Bean对象。这样下次getBean时就可以直接从Map中拿到对应的Bean对象了(实际上,在Spring源码中,这个Map就是单例池);
  2. 如果当前Bean是原型Bean,那么后续没有其他动作,不会存入一个Map,下次getBean时会再次执行上述创建过程,得到一个新的Bean对象。

在这里插入图片描述

4.1 推断构造方法过程细讲

Spring在基于某个类生成Bean的过程中,需要利用该类的构造方法来实例化得到一个对象,但是如果一个类存在多个构造方法,Spring会使用哪个呢?

Spring的判断逻辑如下:

  1. 如果一个类只有一个构造函数,不管构造函数是有参,还是无参,Spring都会使用这个构造函数创建对象,因为没得选了;
  2. 如果这个类存在多个构造函数:
    • 如果存在无参构造函数,则使用无参构造函数。因为在Java里面,无参构造函数本身就具有默认的意思在里面;
    • 如果没有无参构造函数,则看多个无参构造函数,哪个有@Autowired修饰,有就选择;没有就只能报错了

还有一个问题。如果Spring选择了一个有参的构造方法,Spring在调用这个有参构造方法时,需要传入参数,那这个参数是怎么来的呢?答案是:Spring会根据入参的类型和入参的名字去Spring中找Bean对象。
3. 先根据入参类型找,如果只找到一个,那就直接用来作为入参;
4. 如果根据类型找到多个,则再根据入参名字来确定唯一一个;
5. 最终如果没有找到,则会报错,无法创建当前Bean对象。

五、【AOP】过程简单推测

AOP就是进行动态代理,在创建一个Bean的过程中,Spring在最后一步(放入单例池之前)会去判断当前正在创建的这个Bean是不是需要进行AOP,如果需要则会进行动态代理。
那么,如何判断一个Bean是否需要被AOP代理呢?步骤如下:

  1. 找出所有的切面Bean(切面也是Bean来的,或者叫做:特殊的Bean)
  2. 遍历切面中的每个方法,看是否写了@Before、@After等注解(通知)
  3. 如果写了,则判断所对应的Pointcut是否和当前Bean对象的类是否匹配
  4. 如果匹配则表示当前Bean对象有匹配的的Pointcut,表示需要进行AOP

利用cglib进行AOP的大致流程:(看上面的代理范式大概就知道了)

  1. 新增一个代理类XxxProxy,继承自被代理对象XxxTarget,并且持有一个XxxTarget成员变量(这个成员变量需要经过一个Bean的声明周期,即,完成了IOC等)
  2. 在代理类中重写父类的方法
  3. 执行代理类的方法时,调用的代理类的方法,但同时也需要执行切面的逻辑

在这里插入图片描述

然后这里给大家一个【代理模式】的范式:

// 被代理对象
public class ProxyTarget {public void run() {System.out.println("这是普通对象的run");}
}// 代理对象
public class ProxyModel extends ProxyTarget {private ProxyTarget proxyTarget;public void setProxyTarget(ProxyTarget proxyTarget) {this.proxyTarget = proxyTarget;}@Overridepublic void run() {System.out.println("我代理对象可以在这里做加强---1");super.run();System.out.println("我代理对象也可以在这里做加强---2");}
}

六、Spring事务

当我们在某个方法上加了@Transactional注解后,就表示该方法在调用时会开启Spring事务,而这个方法所在的类所对应的Bean对象会是该类的代理对象。
Spring事务的代理对象执行某个方法时的步骤:

  1. 判断当前执行的方法是否存在@Transactional注解
  2. 如果存在,则利用事务管理器(TransactionMananger)新建一个数据库连接
  3. 修改数据库连接的autocommit为false
  4. 执行target.test(),执行程序员所写的业务逻辑代码,也就是执行sql
  5. 执行完了之后如果没有出现异常,则提交,否则回滚

Spring事务是否会失效的判断标准:某个加了@Transactional注解的方法被调用时,要判断到底是不是直接被代理对象调用的,如果是则事务会生效,如果不是则失效。(PS:这一点很容易被疏忽)
另外,还有个经典例子,那就是@Bean在有跟没有@Configuration的时候,结果是不一样的,如下:

声明Bean的方法:

@ComponentScan("org.tuling.spring")
@Configuration
public class AppConfig {@Beanpublic UserService userService() {return new UserService(walletService());}@Beanpublic UserService userService1() {return new UserService(walletService());}@Beanpublic WalletService walletService() {return new WalletService();}
}// UserService声明
public class UserService {private WalletService walletService;public UserService() {}public UserService(WalletService walletService) {this.walletService = walletService;}public WalletService getWalletService() {return walletService;}/*** 自我介绍*/public void selfIntroduction() {System.out.println("你好,我是阿通,我有好多钱");walletService.showMyBalance();}
}

大家看上面的声明Bean的方法,按照设想,WalletService肯定也是一个单例嘛,所以,userServiceuserService1持有的walletService对象肯定是一样的。

调用方法:

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);UserService userService = (UserService)context.getBean("userService");System.out.println(userService);System.out.println(userService.getWalletService());System.out.println("--------------------------------");UserService userService1 = (UserService)context.getBean("userService1");System.out.println(userService1);System.out.println(userService1.getWalletService());

结果输出如下:

org.tuling.spring.bean.UserService@2c34f934
org.tuling.spring.bean.WalletService@12d3a4e9
--------------------------------
org.tuling.spring.bean.UserService@240237d2
org.tuling.spring.bean.WalletService@12d3a4e9

看结果,没什么问题,如期输出。但如果我们把声明Bean的方法里面的@Configuration去掉,结果会变成这样:

@ComponentScan("org.tuling.spring")
//@Configuration
public class AppConfig {@Beanpublic UserService userService() {return new UserService(walletService());}@Beanpublic UserService userService1() {return new UserService(walletService());}@Beanpublic WalletService walletService() {return new WalletService();}
}
org.tuling.spring.bean.UserService@710726a3
org.tuling.spring.bean.WalletService@646007f4
--------------------------------
org.tuling.spring.bean.UserService@481a15ff
org.tuling.spring.bean.WalletService@78186a70

为什么,只是简单注释了一个@Configuration结果就不一样了呢?分析如下:

  1. @Bean注解可以将方法返回的对象注册为一个 Bean,并且该 Bean 会被 Spring 容器管理。仅此而已
  2. 所以,在userService()方法重调用walletService()方法,实际上就是一个普通Java调用而已,肯定会重新new WalletService()
  3. 而被@Configuration注解之后,所有方法都将被代理(暂时还没找到源码证据,等后面我看懂了再附上)

学习总结

  1. 简单学习了Spring启动的流程
  2. 通过一些常见Spring操作的串讲,大概了解了一下IOC和AOP的大致流程

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

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

相关文章

分布式 - 消息队列Kafka:Kafka生产者架构和配置参数

文章目录 1. kafka 生产者发送消息整体架构2. Kafka 生产者重要参数配置01. acks02. 消息传递时间03. linger.ms04. buffer.memory05. batch.size06. max.in.flight.requests.per.connection07. compression.type08. max.request.size09. receive.buffer.bytes和 send.buffer.b…

uniapp 微信小程序 封装公共的请求js(api版本)

一、新建api文件夹 在项目目录下创建api文件夹&#xff0c;内放files跟index.js文件夹&#xff0c;files文件夹内放每个页面对应的js请求接口 1、index.js /*** api接口的统一出口*/ const api {}; const requireComponent require.context(./files, false, /\.js$/) requi…

ssm亚盛汽车配件销售业绩管理统源码和论文PPT

ssm亚盛汽车配件销售业绩管理统源码和论文PPT007 开发工具&#xff1a;idea 数据库mysql5.7(mysql5.7最佳) 数据库链接工具&#xff1a;navcat,小海豚等 开发技术&#xff1a;java ssm tomcat8.5 研究的意义 汽车配件销售类企业近年来得到长足发展,在市场份额不断扩大同时…

『PostgreSQL』在 PostgreSQL中创建只读权限和读写权限的账号

&#x1f4e3;读完这篇文章里你能收获到 理解在 PostgreSQL 数据库中创建账号的重要性以及如何进行账号管理掌握在 PostgreSQL 中创建具有只读权限和读写权限的账号的步骤和方法学会使用 SQL 命令来创建账号、为账号分配适当的权限以及控制账号对数据库的访问级别了解如何确保…

HBase-写流程

写流程顺序正如API编写顺序&#xff0c;首先创建HBase的重量级连接 &#xff08;1&#xff09;读取本地缓存中的Meta表信息&#xff1b;&#xff08;第一次启动客户端为空&#xff09; &#xff08;2&#xff09;向ZK发起读取Meta表所在位置的请求&#xff1b; &#xff08;…

MySQL 建表 及其 表的约束类型

目录 步骤&#xff1a; 1、选择数据库(mydb--自定义数据库) 2、建立班级表 3、建立学生表 4、增加约束删除约束 增添约束&#xff1a; 删除约束&#xff1a; 以班级表和学生表为例说明表的约束类型 步骤&#xff1a; 1、选择数据库(mydb--自定义数据库) 2、建立班级表 …

【图像去噪的扩散滤波】基于线性扩散滤波、边缘增强线性和非线性各向异性滤波的图像去噪研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

SQL SERVER ip地址改别名

SQL server在使用链接服务器时必须使用别名&#xff0c;使用ip地址就会把192.188.0.2这种点也解析出来 解决方案&#xff1a; 1、物理机ip 192.168.0.66 虚拟机ip 192.168.0.115 2、在虚拟机上找到 C:\Windows\System32\drivers\etc 下的 &#xff08;我选中的文件&a…

maven Jar包反向install到本地仓库

maven Jar包反向install到本地仓库 需求实现 需求 项目打包时报错&#xff0c;缺少一个jar包。 但是在maven仓库都找不到此jar包&#xff0c;其他人提供了这个jar包。 需要把这个jar包install到本地仓库&#xff0c;使项目能正常打包运行。 实现 使用git bash命令执行以下脚…

24聊城大学823软件工程考研

1.软件发展有几个阶段&#xff1f;各有何特征&#xff1f; ①程序设计阶段 硬件特征&#xff1a;价格贵、存储容量小、运行可靠性差。 软件特征&#xff1a;只有程序、程序设计概念&#xff0c;不重视程序设计方法。 ②程序系统阶段。 硬件特征&#xff1a;速度、容量及工作可…

Redis的RDB持久化

Redis是一个键值对数据库服务器&#xff0c;服务器中通常包含着任意个非空数据库&#xff0c;而每个非空数据库中又可以包含任意个键值对&#xff0c;为了方便起见&#xff0c;我们将服务器中的非空数据库以及它们的键值对统称为数据库状态。 举个例子&#xff0c;下图展示了一…

使用自己的数据集预加载 Elasticsearch

作者&#xff1a;David Pilato 我最近在讨论论坛上收到一个问题&#xff0c;关于如何修改官方 Docker 镜像以提供一个现成的 Elasticsearch 集群&#xff0c;其中已经包含一些数据。 说实话&#xff0c;我不喜欢这个想法&#xff0c;因为你必须通过提 entrypoint.sh 的分叉版本…

一文详解 DolphinDB SQL 标准化

1. 与标准 SQL 的兼容性 在脚本层面&#xff0c;自 1.30.22 / 2.00.10 版本起&#xff0c;DolphinDB SQL 开始支持&#xff1a; 关键字全大写或全小写的书写方式脚本的换行不影响语法解析&#xff0c;但存在两种特殊情况&#xff1a; 组合关键字不允许拆分&#xff0c;如 ord…

PromQL实现Actuator获取的JVM指标的Full GC次数监控

Spring Boot 版本需要2.0.0或更高版本。 添加Micrometer Prometheus registry依赖: <dependency><groupId>io.micrometer</groupId><artifactId>micrometer-registry-prometheus</artifactId> </dependency>在application.properties中开…

去趋势化一个心电图信号、信号功率谱、低通IIR滤波器并平滑信号、对滤波器引起的延迟进行补偿研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

嗅探抓包工具,解决线上偶现问题来不及抓包的情况阅读目录

目录 背景 实现思路 具体实现 Python 抓包 总结 资料获取方法 背景 测试群里经常看到客户端的同学反馈发现了偶现Bug&#xff0c;但是来不及抓包&#xff0c;最后不了了之&#xff0c;最近出现得比较频繁&#xff0c;所以写个小脚本解决这个问题。 实现思路 之前写过一个…

IDEA快捷键总结

切换窗口 Alt&#xff08;1-9&#xff09; Alt1 打开或者关闭左侧project Alt4 Run窗口 Alt5 Debug窗口 Alt7 类结构窗口 生成构造函数、get、set等方法 Altinsert 快速生成输出语句 Soutenter键 运行程序 chtlshiftf10 运行程序 shiftf9 debug方式运行程序 代码…

汽车控制器底层软件BOOTLOADER开发经历

引言 现在所谓智能汽车必备的OTA技术&#xff0c;在ECU控制器层面就是BOOT的开发&#xff0c;对应autosar体系里面的BSW基础软件。 同学刚开始接触汽车软件开发会有一种思想&#xff0c;要学就学听起来high level的autosar&#xff0c;但是到底autosar是个什么东西也搞不懂&am…

排序的介绍

排序算法介绍 排序是计算机内经常进行的一种操作&#xff0c;其目的是将一组“无序”的记录序列调整为“有序”的记录序列 粗暴理解 将杂乱无章的数据元素&#xff0c;通过一定的方法按照关键字顺序排列的过程叫做排序 排序分内部排序和外部排序&#xff0c;若整个排序过程不需…

Java算法_ LRU 缓存(LeetCode_Hot100)

题目描述&#xff1a;请你设计并实现一个满足 LRU &#xff08;最近最少使用&#xff09; 缓存 约束的数据结构。 获得更多&#xff1f;算法思路:代码文档&#xff0c;算法解析的私得。 运行效果 完整代码 import java.util.HashMap; import java.util.Map;/*** 2 * Author: L…