系统学习SpringFramework:循环依赖与三级缓存

本篇内容包括:Spring 中的循环依赖问题(包括 Spring 中的循环依赖问题和Spring 中的循环依赖的 5 种场景的介绍)、Spring 三级缓存介绍、4 个 Spring 无法自动解决的循环以来场景以及其对应的手动解决方式。

一、Spring 中的循环依赖问题

1、Spring 中的循环依赖概述

Spring 循环依赖指的是 SpringBean 对象之间的依赖关系形成一个闭环。即在代码中,把两个或者多个 Bean 相互之间去持有对方的引用,就会发生循环依赖,循环依赖会导致注入出现死循环,这是 Spring 发生循环依赖的主要原因之一。

Spring 循环依赖主要有三种情况,即:自身依赖自身,两者互相依赖,多者循环依赖

  1. 自身依赖自身:自己依赖自己的直接依赖
  2. 两者互相依赖:两个对象之间的直接依赖
  3. 多者循环依赖:多个对象之间的间接依赖

Spring循环依赖问题

自身依赖自身,两者互相依赖 两者互相依赖的情况比较直观,很好辨识,但是我们工作中最有可能触发的还是多者循环依赖,多者循环依赖的情况有时候因为业务代码调用层级很深,不容易识别出来。但无论循环依赖的数量有多少,循环依赖的本质是一样的。就是你的完整创建依赖于我,而我的完整创建也依赖于你,但我们互相没法解耦,最终导致依赖创建失败。

2、Spring 中的循环依赖的 5 种场景

Spring 中出现循环依赖主要有着 5 种场景: ①、单例的 setter 注入(能解决);②、多例的 setter 注入(不能解决);③、构造器注入(不能解决);④、单例的代理对象 setter 注入(有可能解决);⑤、DependsOn 循环依赖(不能解决)。接下来我们逐一来看。

img

二、Spring 三级缓存

1、spring 创建 bean 的流程

在开始理解 Spring 三级缓存如何让解决循环依赖问题前我们先来温习一下 spring 创建 bean 的流程:

  1. Spring 启动时会根据配置文件或启动类把所有的 bean 注册成 bean 定义(就是映射 <bean> 标签属性的 Java 类)

  2. 遍历 bean 定义中的 beanName,调用 BeanFactory#getBean(beanName) 方法创建、初始化并返回 bean 实例

    其中 getBean 方法:

    1. 先从缓存(一层到三层依次获取)拿,没有就去创建;
    2. 创建 Bean 时,把 beanName 标记为正在创建中,通过其定义里的 class 找到构造器方法反射创建实例,并把其对象工厂放入第三层缓存
    3. 对实例初始化,移除正在创建中的标记,把实例放入第一层缓存,移除第二、三层中的缓存,最后返回实例

Ps1:实例初始化过程:获取此 bean 中有 @Autowired 等注解的成员变量,从所有 bean 定义中找出此类型的 beanName,又通过 BeanFactory#getBean 方法获取实例,然后反射设值成员变量。

Ps2:上述流程中 斜体 部分为触发循环依赖时多出主流程的步骤。

位于 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry 中的三级缓存源码:

	/** Cache of singleton objects: bean name to bean instance. */private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);/** Cache of singleton factories: bean name to ObjectFactory. */private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);/** Cache of early singleton objects: bean name to bean instance. */private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

2、场景一:单例的 setter 注入

这种注入方式应该是 Spring 中最常见的,Demo 如下:

@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}

在上述代码中,就是一个经典的循环依赖,其中 TestService1 依赖 TestService2,TestService2 依赖 TestService1 构成了一个简单了两者互相依赖关系,但是我们在使用类似代码时,并没有感知过该类型的循环依赖存在,因为此种类型已经被 Spring 默默解决了。

3、三级缓存

Spring 内部有三级缓存:

  • 一级缓存(singletonObjects),用于保存实例化、注入、初始化完成的 Bean 实例
  • 二级缓存(earlySingletonObjects),用于保存实例化完成的 Bean 实例
  • 三级缓存(singletonFactories),用于保存 Bean 的创建工厂,以便于后面扩展有机会创建代理对象。

以上面 Demo 为例,现在项目启动,spring 开始创建 bean,比如先创建 TestService1:

  1. 标记 TestService1 为正在创建中,反射创建其实例,其对象工厂放入第三层缓存
  2. 初始化 TestService1 实例化时发现需要依赖注入 TestService2,则获取 TestService2 的实例
  3. 标记 TestService2 为正在创建中,反射创建其实例,其对象工厂放入第三层缓存
  4. 初始化 TestService2 实例化时发现需要依赖注入 TestService1,则获取 TestService1 的实例
  5. 这时候从缓存中获取时,TestService1 为正在创建中且第三层缓存有 TestService1 的值了,所以调用缓存的对象工厂的 getObject 方法,把返回的 TestService1 实例放入第二层缓存,删除第三层缓存
  6. TestService2 实例初始化完成,放入第一层缓存,移除第二、三层中的缓存
  7. 回到第 2 步,TestService1 实例初始化完成,放入第一层缓存,移除第二、三层中的缓存

img

下面是 getBean(beanName) 方法最先调用的从这三层缓存中获取 bean 实例的逻辑(即上面第5步)

	/*** Return the (raw) singleton object registered under the given name.* <p>Checks already instantiated singletons and also allows for an early* reference to a currently created singleton (resolving a circular reference).* @param beanName the name of the bean to look for* @param allowEarlyReference whether early references should be created or not* @return the registered singleton object, or {@code null} if none found*/@Nullableprotected Object getSingleton(String beanName, boolean allowEarlyReference) {// Quick check for existing instance without full singleton lockObject singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {synchronized (this.singletonObjects) {// Consistent creation of early reference within full singleton locksingletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) {ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {singletonObject = singletonFactory.getObject();this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);}}}}}}return singletonObject;}

以及一直提到的对象工厂,及其 getObject 方法的实现:

	/*** Obtain a reference for early access to the specified bean,* typically for the purpose of resolving a circular reference.* @param beanName the name of the bean (for error handling purposes)* @param mbd the merged bean definition for the bean* @param bean the raw bean instance* @return the object to expose as bean reference*/protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {Object exposedObject = bean;if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {for (BeanPostProcessor bp : getBeanPostProcessors()) {if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);}}}return exposedObject;}

4、关于二级缓存

细心的朋友可能会发现在这种场景中第二级缓存作用不大。那么问题来了,为什么要用第二级缓存呢?

试想一下,如果出现以下这种情况,我们要如何处理?

@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;@Autowiredprivate TestService3 testService3;public void test1() {}
}
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}
@Service
public class TestService3 {@Autowiredprivate TestService1 testService1;public void test3() {}
}

TestService1 依赖于 TestService2 和 TestService3,而 TestService2 依赖于 TestService1,同时 TestService3 也依赖于 TestService1。按照上图的流程可以把 TestService1 注入到 TestService2,并且 TestService1 的实例是从第三级缓存中获取的。

假设不用第二级缓存,TestService1 注入到 TestService3 的流程如图:

img

TestService1 注入到 TestService3 又需要从第三级缓存中获取实例,而第三级缓存里保存的并非真正的实例对象,而是 ObjectFactory对象。说白了,两次从三级缓存中获取都是 ObjectFactory 对象,而通过它创建的实例对象每次可能都不一样的。这样不是有问题?

为了解决这个问题,Spring 引入的第二级缓存。上面其实 TestService1 对象的实例已经被添加到第二级缓存中了,而在 TestService1 注入到 TestService3 时,只用从第二级缓存中获取该对象即可。

img

还有个问题,第三级缓存中为什么要添加 ObjectFactory 对象,直接保存实例对象不行吗?答:不行,因为假如你想对添加到三级缓存中的实例对象进行增强,直接用实例对象是行不通的。


三、循环依赖的其他 4 种场景

1、多例的 setter 注入

这种注入方法偶然会有,特别是在多线程的场景下,具体代码如下:

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}

在上述多例的 setter 注入情况下,Spring 程序也是能够正常启动启动的,其实在 AbstractApplicationContext 类的 refresh方法中告诉了我们答案,它会调用 finishBeanFactoryInitialization 方法,该方法的作用是为了 Spring 容器启动的时候提前初始化一些 Bean。该方法的内部又调用了 preInstantiateSingletons 方法

@Overridepublic void preInstantiateSingletons() throws BeansException {if (logger.isTraceEnabled()) {logger.trace("Pre-instantiating singletons in " + this);}// Iterate over a copy to allow for init methods which in turn register new bean definitions.// While this may not be part of the regular factory bootstrap, it does otherwise work fine.List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);// Trigger initialization of all non-lazy singleton beans...for (String beanName : beanNames) {RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {if (isFactoryBean(beanName)) {Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);if (bean instanceof FactoryBean) {FactoryBean<?> factory = (FactoryBean<?>) bean;boolean isEagerInit;if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>) ((SmartFactoryBean<?>) factory)::isEagerInit,getAccessControlContext());}else {isEagerInit = (factory instanceof SmartFactoryBean &&((SmartFactoryBean<?>) factory).isEagerInit());}if (isEagerInit) {getBean(beanName);}}}else {getBean(beanName);}}}

其中 非抽象、单例 并且非懒加载的类才能被提前初始 Bean。,而多例即 SCOPE_PROTOTYPE 类型的类,非单例,不会被提前初始化 Bean,所以程序能够正常启动。如何让他提前初始化bean呢?

只需要再在 DEMO 中定义一个单例的类,在它里面注入 TestService1

@Service
public class TestService3 {@Autowiredprivate TestService1 testService1;
}

重新启动程序,执行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

果然出现了循环依赖。

Ps:这种循环依赖问题是无法解决的,因为它没有用缓存,每次都会生成一个新对象。

2、构造器注入

这种注入方式现在其实用的已经非常少了,但是我们还是有必要了解一下,如下代码:

@Service
public class TestService1 {public TestService1(TestService2 testService2) {}
}
@Service
public class TestService2 {public TestService2(TestService1 testService1) {}
}

运行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

出现了循环依赖,为什么呢?

img

从图中的流程看出构造器注入没能添加到三级缓存,也没有使用缓存,所以也无法解决循环依赖问题。

3、单例的代理对象 setter 注入

这种注入方式其实也比较常用,比如平时使用:@Async 注解的场景,会通过 AOP 自动生成代理对象。

@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}
}
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}

从前面得知程序启动会报错,出现了循环依赖,为什么会循环依赖呢?答案就在下面这张图中:

img

说白了,Bean 初始化完成之后,后面还有一步去检查:第二级缓存和原始对象是否相等。由于它对前面流程来说无关紧要,所以前面的流程图中省略了,但是在这里是关键点,我们重点说说:

if (earlySingletonExposure) {Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {if (exposedObject == bean) {exposedObject = earlySingletonReference;}else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {String[] dependentBeans = getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);for (String dependentBean : dependentBeans) {if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}if (!actualDependentBeans.isEmpty()) {throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] in its raw version as part of a circular reference, but has eventually been " +"wrapped. This means that said other beans do not use the final version of the " +"bean. This is often the result of over-eager type matching - consider using " +"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");}}}}

正好是走到这段代码,发现第二级缓存和原始对象不相等,所以抛出了循环依赖的异常。如果这时候把 TestService1 改个名字,改成:TestService6,其他的都不变。

@Service
public class TestService6 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}
}

再重新启动一下程序,神奇般的好了。这是为什么?这就要从 Spring Bean 加载顺序说起了,默认情况下,Spring 是按照文件完整路径递归查找的,按路径+文件名排序,排在前面的先加载。所以 TestService1 比T estService2 先加载,而改了文件名称之后,TestService2 比 TestService6 先加载。

为什么 TestService2 比 TestService6 先加载就没问题呢?答案在下面这张图中:

img

这种情况 testService6 中其实第二级缓存是空的,不需要跟原始对象判断,所以不会抛出循环依赖。

4、DependsOn 循环依赖

还有一种有些特殊的场景,比如我们需要在实例化 Bean A 之前,先实例化 Bean B,这个时候就可以使用 @DependsOn 注解。

@DependsOn(value = "testService2")
@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}
@DependsOn(value = "testService1")
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}

程序启动之后,执行结果:

Circular depends-on relationship between 'testService2' and 'testService1'

这个例子中本来如果 TestService1 和 TestService2 都没有加 @DependsOn 注解是没问题的,反而加了这个注解会出现循环依赖问题。

这又是为什么?答案在 AbstractBeanFactory 类的 doGetBean 方法的这段代码中:

// Guarantee initialization of beans that the current bean depends on.String[] dependsOn = mbd.getDependsOn();if (dependsOn != null) {for (String dep : dependsOn) {if (isDependent(beanName, dep)) {throw new BeanCreationException(mbd.getResourceDescription(), beanName,"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");}registerDependentBean(dep, beanName);try {getBean(dep);}catch (NoSuchBeanDefinitionException ex) {throw new BeanCreationException(mbd.getResourceDescription(), beanName,"'" + beanName + "' depends on missing bean '" + dep + "'", ex);}}}

它会检查 dependsOn 的实例有没有循环依赖,如果有循环依赖则抛异常。


三、出现循环依赖如何解决?

项目中如果出现循环依赖问题,说明是 Spring 默认无法解决的循环依赖,要看项目的打印日志,属于哪种循环依赖。目前包含下面几种情况:

img

解决方式:

问题解决方式
生成代理对象产生的循环依赖①、 使用 @Lazy 注解,延迟加载 ②、使用 @DependsOn 注解,指定加载先后关系 ③、修改文件名称,改变循环依赖类的加载顺序
多例循环依赖可以通过把 Bean 改成单例的解决
构造器循环依赖可以通过使用 @Lazy 注解解决
使用 @DependsOn 产生的循环依赖要找到@DependsOn注解循环依赖的地方,迫使它不循环依赖就可以解决问题

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

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

相关文章

oracle安装需要的包列表

redhat7.5安装图形界面&#xff1a; [rootwwyt ~]# rpm -ivh --nodeps --force xorg-x11-font* [rootwwyt ~]# mount -o loop -t iso9660 rhel-server-7.5-x86_64-dvd.iso /media/iso/ [rootwwyt ~]# cat /etc/yum.repos.d/my.repo [base] nameredhat7.5 baseurlfile:///m…

深入理解Java虚拟机:Java类的加载机制

本篇内容包括&#xff1a;Java 类的加载机制&#xff08;Jvm 结构组成、Java 类的加载&#xff09;、类的生命周期&#xff08;加载-验证-准备-解析-初始化-使用-卸载&#xff09;、类加载器 以及 双亲委派模型。 一、Java 类的加载机制 1、 Jvm 结构组成 Jvm 整体组成可分为…

新版谷歌浏览器开启Flash支持

浏览器地址栏中输入chrome://version查看Chrome浏览器、Flash插件的版本信息。 Chrome 69.0-70.0版本Chrome 71.0-74.0及以后版本谷歌浏览器地址栏中输入【chrome://flags/#enable-ephemeral-flash-permission】&#xff0c;将【Enable Ephemeral Flash Permissions】从【Defau…

深入理解Java虚拟机:Java垃圾回收机制

本篇内容包括&#xff1a;JAVA 垃圾回收机制概述、有哪些内存需要回收、如何回收&#xff08;标记-清除、标记-整理&#xff08;标记-清除-压缩&#xff09;、复制&#xff08;标记-复制-清除&#xff09;、分代收集等算法&#xff09; 以及 何时进行垃圾回收等内容&#xff01…

深入理解Java虚拟机:Java垃圾回收器

本篇内容包括&#xff1a;7 种 Jvm 垃圾回收器的介绍、对比 以及 对应的 Jvm 参数设置&#xff0c;这 7 种包括了&#xff1a;Serial、ParNew 以及 Parallel Scavenge 三种新生代回收器 和 &#xff1a;Serial Old、Parallel Old 以及 CMS 三种老年代回收器&#xff0c;此外还有…

oracle跨越千年处理

如果指定的两位年份0-4950-99 如果当前 的两位年 份是 0-49返回的日期是当前世纪返回的日期是上个世纪50-99返回的日期是下个世纪返回的日期是当前世纪 current yearSpecified DateRR FormatYY Format199527-OCT-9519951995199527-OCT-171951917200127-OCT-1720012017200127-OC…

网络协议:什么是网络分层的七四五

本篇内容包括&#xff1a;网络分层七层、五层、四层网络协议概念的介绍&#xff0c;IOS 体系结构的介绍与构成、TCP/IP体系结构的简介及与IOS体系的关系 以及五层体系结构的介绍。 一、七层、五层、四层网络协议概念 1、关于网络协议 网络协议&#xff0c;即是指计算机网络中…

查看表空间相关命令

默认表空间数据文件大小根据DATA BLOCKS的大小有关&#xff0c;默认最大为32GB表空间达到32G&#xff0c;只能增加数据文件alter tablespace 表空间名 add datafile 数据文件路径‘ size 500m autoextend on next 100m maxsize 10000M;未达到32G&#xff0c;修改数据文件的扩展…

网络协议:一文搞懂Socket套接字

本篇内容包括&#xff1a;Socket 套接字的简介、Socket 套接字的分类、Java 中的 Socket 即 java.net.ServerSocket、java.net.Socket 的使用&#xff0c;以及Java 使用套接字 Scoket 编程的Demo。 一、Socket 简介 TCP&#xff08;传输控制协议&#xff09;是一种面向连接的、…

RESETLOGS

使用resetlogs选项&#xff0c;会把当前的日志序号&#xff08;log sequence number&#xff09;重设为1&#xff0c;并抛弃所有日志信息。在以下条件时需要使用resetlogs选项&#xff1a; 在不完全恢复&#xff08;介质恢复&#xff09;&#xff1b; 使用备份控制文件。 使…

网络协议:透彻解析HTTP协议

本篇内容包括&#xff1a;HTTP 协议定义及其特点概述、关于 URL 定义及分类概述、Request 请求、Response 响应 以及 浏览器访问一个网站的全过程 等内容… 一、HTTP 协议概述 HTTP&#xff08;HyperText Transfer Protocol&#xff09; 即 超文本传输协议&#xff0c;它是一种…

oracle参数文件和口令文件

外部 审核 口令&#xff1a;记录超级用户的用户名和口令&#xff0c;做sys用户的安全审核 oracle9以后全部使用sys登录&#xff0c;但需要使用as sysdba &#xff0c;之前版本需要使用internal o7字典打开 只要用户和密码存在于口令文件&#xff0c;就可以以sysdba登录&#…

innobackup备份恢复实操步骤--gtid复制(1)(1)

首先在主库进行备份&#xff1a; 备份命令&#xff1a; Innobackupex --defaults-file/app/dbcluster/sgrdb/mysql/my19103.cnf --no-timestamp --userdbscale --passwordS6000dbscale --host10.157.43.224 --port19103 /data/backup 如果使用setsid&#xff1a; setsid …

Redis系列:Redis的概述与安装

Redis(Remote Dictionary Server) 是一个使用 C 语言编写的&#xff0c;开源的&#xff08;BSD许可&#xff09;高性能非关系型&#xff08;NoSQL&#xff09;的键值对数据库。 本篇内容包括&#xff1a;Redis 简介&#xff08;为什么快&#xff1f;为什么单线程&#xff1f;优…

安装LibreOffice和字体

#/bin/bash # Check if user is root if [ $(id -u) ! "0" ]; thenecho "Error: You must be root to run this script, please use root"exit 1 fi echo 安装LibreOffice cd /home/ tar -zxvf LibreOffice_6.3.3_Linux_x86-64_rpm.tar.gz cd /home/LibreO…

xtrabackup备份脚本

#!/bin/sh #备份主机 remote_ip100.0.132.160 Master_ip100.20.132.158 VIP100.20.132.166 #备份用户 userroot #密码 password00000 # 返回年月日 backup_datedate %F # 返回时分秒 backup_timedate %H-%M-%S # 返回今天是这周的第几天 backup_week_daydate %u backup_ok0 #备…

Redis系列:使用Redis实现分布式锁及相关问题

分布式锁其实就是&#xff0c;控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源&#xff0c;往往需要互斥来防止彼此干扰&#xff0c;以保证一致性。 本篇内容包括&#xff1a;关于 Redis 与 分布式锁&…

Redis系列:Redis持久化机制与Redis事务

Redis 是个基于内存的数据库。那服务一旦宕机&#xff0c;内存中数据必将全部丢失。所以丢失数据的恢复对于 Redis 是十分重要的&#xff0c;我们首先想到是可以从数据库中恢复&#xff0c;但是在由 Redis 宕机时&#xff08;说明相关工作正在运行&#xff09;且数据量很大情况…

Java基础:Java程序设计环境

按应用范围&#xff0c;Java 可分为 3 个体系&#xff0c;即 Java SE、Java EE 和 Java ME。Java 语言的开发运行&#xff0c;也离不开 Java 语言的运行环境 JRE。没有 JRE 的支持&#xff0c;Java 语言便无法运行。当然&#xff0c;如果还想编译 Java 程序&#xff0c;搞搞小开…