Java开发过程中,我们常常需要借助Spring框架来实现业务需求。其中,AOP和IOC是两个非常重要的思想。那么,在Java中,我们是如何实现这两个思想的呢?本文将重点讲解IOC的概念,并结合Spring底层源码,分析其实现过程,同时介绍不同IOC实现方式可能存在的问题。
IOC(Inversion of Control),即控制反转。它是一种设计思想,其核心思想是将对象的创建、依赖注入等一系列操作交由框架来完成。这样,我们就可以将精力集中在业务逻辑的编写上,而不必过多关注对象创建等底层细节。
IOC容器如何存
在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。
注入的核心其实就是解析 xml 文件的内容,找到 元素,然后经过一系列加工,最后把这些加工后的对象存到一个公共空间,供调用者获取使用。
而至于使用注解方式的 bean,比如使用 @Bean
、@Service
、@Component
等注解的,只是解析这一步不一样而已,剩下的操作基本都一致。
这个公共空间是一个 Map,而且是一个 ConcurrentHashMap ,为了保证并发安全。它的声明如下,在 DefaultListableBeanFactory
中。
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256)
其中 beanName 作为 key,也就是例子中的 logger,value 是 BeanDefinition 类型,BeanDefinition 用来描述一个 Bean 的定义,我们在 xml 文件中定义的 元素的属性都在其中,还包括其他的一些必要属性。
向 beanDefinitionMap 中添加元素的过程,叫做 Bean 的注册,只有被注册过的 Bean 才能被使用。
实例化的Bean存储在哪?
有一个 Map 叫做 singletonObjects,其声明如下:
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
在 refresh() 过程中,还会将 Bean 存到这里一份,这个存储过程发生在 finishBeanFactoryInitialization(beanFactory) 方法内,它的作用是将非 lazy-init 的 Bean 放到singletonObjects 中。
除了存我们定义的 Bean,还包括几个系统 Bean。
什么是Spring Bean?
Bean 代指的就是那些被 IoC 容器所管理的对象。
我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。
<!-- Constructor-arg with 'value' attribute -->
<bean id="..." class="...">
<constructor-arg value="..."/>
</bean>
下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。
org.springframework.beans
和 org.springframework.context
这两个包是 IoC 实现的基础,
Java依赖注入(Dependency Injection,DI)是指在一个对象需要依赖另一个对象时,由容器自动将依赖的对象注入到该对象中。常见的实现方式有构造函数注入、属性注入和接口注入等。
现在已经很少项目用 xml 这种配置方式了,基本上都是 Spring Boot,就算不用,也是在 Spring MVC 中用注解的方式注册、使用 Bean 了。其实整个过程都是类似的,只不过注册和获取的时候多了注解的参与。Srping 中 BeanFactory
和ApplicationContext
都是接口,除此之外,还有很多的抽象类,使得我们可以灵活的定制属于自己的注册和调用流程,可以认为注解方式就是其中的一种定制。只要找到时机解析好对应的注解标示就可以了。
但是看 Spring Boot 的注册和调用过程没有 xml 方式的顺畅,这都是因为注解的特性决定的。注解用起来简单、方便,好处多多。但同时,注解会割裂传统的流程,传统流程都是一步一步主动调用,只要顺着代码往下看就可以了,而注解的方式会造成这个过程连不起来,所以读起来需要额外的一些方法。
IOC又是如何实现的?
像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。注解的实现也用到的反射机制。
在Java中,我们可以通过反射机制来实现IOC。
Spring框架中,IOC容器就是使用了反射机制实现的。当我们在配置文件中定义了一个Bean时,Spring容器就会根据该Bean的配置信息,利用反射机制创建出该Bean的实例,并将其注入到需要该Bean的地方。
不同的IOC实现方式可能会存在一些问题。
- 使用XML配置文件配置Bean时,容易出现配置错误的问题;
- 使用注解配置Bean时,可能会导致配置信息分散在各个类中,不易维护。
在实现一个最简单的IOC时,可以通过以下步骤将给定的名称转换成 bean:
- 定义一个接口或抽象类,表示 bean 的实现
- 实现一个工厂类,用于根据给定的名称生成相应的 bean 实例
- 在工厂类中,使用 Java 的反射机制获取指定名称的类,并创建相应的实例
- 将实例作为 bean 返回
反射
Java的反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法。
优点:
- 能够运行时动态获取类的实例,大大提高系统的灵活性和扩展性。
- 与 Java 动态编译相结合,可以实现无比强大的功能。
- 对于 Java 这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。
缺点:
- 反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
- 反射调用方法时可以忽略权限检查,获取这个类的私有方法和属性,因此可能会破坏类的封装性而导致安全问题。
Java反射是指在程序运行时,根据类的全限定名获取该类的完整定义信息(包括属性、方法、构造方法等),并能够在运行时动态调用这些属性或方法的机制。
Java反射的主要作用是在运行时动态地获取类的信息,从而实现动态创建对象、动态调用方法等操作。Java反射的好处在于它能够提高程序的灵活性和可扩展性,缺点则在于它的运行速度相比于常规的函数调用会更慢。
在Spring开发过程中,反射机制被广泛应用。比如,Spring的IOC容器就是通过反射机制来实现Bean的动态创建和属性注入的。在使用Spring时,我们只需要通过配置文件或注解来定义Bean,Spring就可以利用反射机制创建出Bean的实例,并将其注入到需要使用该Bean的地方。
然而,反射机制也有其缺陷。由于反射机制需要在运行时动态获取类的信息,因此其运行速度相对较慢,且容易导致代码可读性和维护性降低。因此,在使用反射机制时,需要权衡其优缺点,选择最合适的方案。
总的来说,Java反射机制是一种非常强大和灵活的机制,可以帮助我们实现一些常规情况下难以实现的功能。然而,在使用反射时,需要注意其性能和可维护性等问题,以确保程序的稳定性和可扩展性。
可以通过反射访问的常用信息
类型 | 访问方法 | 返回值类型 | 说明 |
---|---|---|---|
包路径 | getPackage() | Package 对象 | 获取该类的存放路径 |
类名称 | getName() | String 对象 | 获取该类的名称 |
继承类 | getSuperclass() | Class 对象 | 获取该类继承的类 |
实现接口 | getlnterfaces() | Class 型数组 | 获取该类实现的所有接口 |
构造方法 | getConstructors() | Constructor 型数组 | 获取所有权限为 public 的构造方法 |
getDeclaredContruectors() | Constructor 对象 | 获取当前对象的所有构造方法 | |
方法 | getMethods() | Methods 型数组 | 获取所有权限为 public 的方法 |
getDeclaredMethods() | Methods 对象 | 获取当前对象的所有方法 | |
成员变量 | getFields() | Field 型数组 | 获取所有权限为 public 的成员变量 |
getDeclareFileds() | Field 对象 | 获取当前对象的所有成员变量 | |
内部类 | getClasses() | Class 型数组 | 获取所有权限为 public 的内部类 |
getDeclaredClasses() | Class 型数组 | 获取所有内部类 | |
内部类的声明类 | getDeclaringClass() | Class 对象 | 如果该类为内部类,则返回它的成员类,否则返回 null |
反射实战
获取Class对象的四种方式
1. 知道具体类的情况下可以使用:
Class alunbarClass = TargetObject.class;
但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化
2. 通过Class.forName()传入类的全路径获取:
Class alunbarClass1 = Class.forName("com.example.TargetObject");
3. 通过对象实例 instance.getClass() 获取:
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
4. 通过类加载
ClassLoader.getSystemClassLoader().loadClass("com.example.reflect");
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行
反射是如何破坏单例模式的?
在正常情况下,单例模式的实现是通过将构造函数私有化,然后通过一个静态方法来获取单例对象的。这样,我们就可以保证在程序运行期间只有一个实例对象。然而,如果使用反射来获取类的实例,就可以绕过单例模式的限制,从而创建出多个实例对象。为了避免这种情况的发生,我们可以在单例类的构造函数中添加一个判断,如果已经创建了一个实例对象,则抛出一个异常,阻止创建新的实例对象。
参考资料:
Spring中IOC容器是如何创建的?_哔哩哔哩_bilibili
《包你懂系列》一文讲清楚 Spring IoC 实现原理和过程 - 掘金 (juejin.cn)