1.1.定义
1.1.1.IoC
Inversion of Control,控制反转。是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(DependencyInjection,简称 DI),这也是 Spring 的实现方式。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
1.1.2.AOP
Aspect Oriented Programming,面向切面编程。通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续,是软件开发中的一个热点,也是 Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。其中,最常用的使用场景一般有日志模块、权限模块、事物模块。
1.2.原理
1.2.1.IoC
IoC 内部核心原理就是反射技术,当然这里面还涉及到 Bean 对象的初始化构建等步骤,这个在后面的生命周期中讲,这里我们需要了解 Java 中反射是如何做的就好。这里主要说明下主要的相关类和可能面试问题转向,具体的 API 实现需要自己去看。
还有其他的类不一一列举出来,都在 java.lang.reflect 包下。说到这个模块的时候,那么面试官可能会考察相关的知识,主要是考察你是否真的有去了解过反射的使用。 举两个例子:
利用反射获取实例的私有属性值怎么做
这里其实就是里面的重要考察点就是反射对私有属性的处理。
/** * 通过反射获取私有的成员变量. */ private Object getPrivateValue(Person person, String fieldName) { try { Field field = person.getClass().getDeclaredField(fieldName); // 主要就是这里,需要将属性的 accessible 设置为 true field.setAccessible(true); return field.get(person); } catch(Exception e) { e.printStackTrace(); } return null; }复制代码
如何通过反射构建对象实例?
使用默认构造函数(无参)创建的话:
Class.newInstance() Constroctor constroctor = clazz.getConstructor(String.class,Integer.class); Object obj = constroctor.newInstance("name", 18);复制代码
1.2.2.AOP
AOP 的内部原理其实就是动态代理和反射了。主要涉及到的反射类:
动态代理相关原理的话,你需要了解什么是代理模式、静态代理的不足、动态代理的实现原理。 Spring 中实现动态代理有两种方式可选,这两种动态代理的实现方式的一个对比也
是面试中常问的。
JDK 动态代理
必须实现 InvocationHandler 接口,然后通过 Proxy.newProxyInstance(ClassLoader
loader, Class<?>[] interfaces, InvocationHandler h) 获得动态代理对象。
CGLIB 动态代理
使用 CGLIB 动态代理,被代理类 不需要强制实现接口。 CGLIB 不能对声明为 final的方法进行代理,因为 CGLIB 原理是动态生成被代理类的子类。
OK,AOP 讲了。其实讲到这里,可能会有一个延伸的面试问题。我们知道,Spring事物也是 通 过 AOP 来 实 现的 , 我们使用的时候 一 般就是在方法上 加@Tranactional 注解,那么你有没有遇到过事物不生效的情况呢?这是为什么?这个问题我们在后面的面试题中会讲。
这只是个大体流程,内部的具体行为太多,需要自行去看看代码。
3.1. 什么是循环依赖,有啥问题?
循环依赖就是 N 个类中循环嵌套引用,这样会导致内存溢出。 循环依赖主要分两种:
无解,直接抛出 BeanCurrentlyInCreatingException 异常。
单例模式下,通过“三级缓存”来处理。非单例模式的话,问题无解。
Spring 初始化单例对象大体是分为如下三个步骤的:
可以看出,循环依赖主要发生在 1、2 步,当然如果发生在第一步的话,Spring 也是无法解决该问题的。那么就剩下第二步 populateBean 中出现的循环依赖问题。通过“三级缓存”来处理,三级缓存如下:
我们看下获取单例对象的方法:
protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this.singletonObjects.get(beanName); // isSingletonCurrentlyInCreation:判断当前单例 bean 是否正在创建中 if(singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized(this.singletonObjects) { singletonObject = this.earlySingletonObjects.get(beanName); // allowEarlyReference:是否允许从 singletonFactories 中通过 getObject 拿到 对象 if(singletonObject == null && allowEarlyReference) { ObjectFactory <? > singletonFactory = this.singletonFactories.get(beanName); if(singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return(singletonObject != NULL_OBJECT ? singletonObject : null); }复制代码
其中解决循环依赖问题的关键点就在 singletonFactory.getObject() 这一步,getObject 这是 ObjectFactory<T> 接口的方法。Spring 通过对该方法的实现,在createBeanInstance 之后,populateBean 之前,通过将创建好但还没完成属性设置和初始化的对象提前曝光,然后再获取 Bean 的时候去看是否有提前曝光的对象实例来判断是否要走创建流程。
protected void addSingletonFactory(String beanName, ObjectFactory <? > singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); synchronized(this.singletonObjects) { if(!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } }复制代码
可能的原因:
按 理 说 , 如 果 按 照 Spring 说 的 事 物 传 播 级 别 去 配 置 其 事 物 级 别 为REQUIRES_NEW 的话,那么应该是在调用 b() 的时候会新生成一个事物。实际上却没有。
NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务其实,这是由于 Spring 的事物实现是通过 AOP 来实现的。此时,当这个有注解的方法 b() 被调用的时候,实际上是由代理类来调用的,代理类在调用之前就会启动 transaction。然而,如果这个有注解的方法是被同一个类中的其他方法 a() 调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个 bean,所以就不会启动 transaction,我们看到的现象就是 @Transactional 注解无效。
Spring 在 2.5 版本以后开始支持用注解的方式来配置依赖注入。可以用注解的方式来替代XML 方式的 bean 描述,可以将 bean 描述转移到组件类的内部,只需要在相关类上、方法上或者字段声明上使用注解即可。注解注入将会被容器在 XML 注入之前被处理,所以后者会覆盖掉前者对于同一个属性的处理结果。注解装配在 Spring 中是默认关闭的。所以需要在 Spring 文件中配置一下才能使用基于注解的装配模式。如果你想要在你的应用程序中使用关于注解的方法的话,请参考如下的配置。
<beans> <context:annotation-config/> <!-- bean definitions go here --> </beans>复制代码
在标签配置完成以后,就可以用注解的方式在 Spring 中向属性、方法和构造方法中自动装配变量。
下面是几种比较重要的注解类型:
具体设计原理如下图:
Spring 使用 ThreadLocal 解决线程安全问题。 我们知道在一般情况下,只有无状态的 Bean 才可以在多线程环境下共享,在Spring 中,绝大部分 Bean 都可以声明为 singleton 作用域。就是因为Spring 对 一 些 Bean ( 如 RequestContextHolder 、
TransactionSynchronizationManager、LocaleContextHolder 等)中非线程安全状态采用 ThreadLocal 进行处理,让它们也成为线程安全的状态,因为有状态的 Bean 就可以在多线程中共享了。
ThreadLocal 和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。而 ThreadLocal 则从另一个角度来解决多线程的并发访问。 ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。 ThreadLocal 提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进 ThreadLocal。
由于 ThreadLocal 中可以持有任何类型的对象,低版本 JDK 所提供的 get()返回的是 Object 对象,需要强制类型转换。但 JDK 5.0 通过泛型很好的解决了这个问题,在一定程度地简化 ThreadLocal 的使用。概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而 ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。