面试题1. Spring中bean的循环依赖怎么解决?
(一). 首先说一下什么是Spring的循环依赖:
其实就是在进行getBean的时候,A对象中去依赖B对象,而B对象又依赖C对象,但是对象C又去依赖A对象,结果就造成A、B、C三个对象都不能完成实例化,出现了循环依赖。就会出现死循环,最终导致内存溢出的错误。
(二).如何去解决Spring的循环依赖呢?
1.先知道什么是Spring的“三级缓存”:就是下面的三个大的Map对象,因为Spring中的循环依赖的理论基础其实是基于java中的值传递的,然后其实Spring中的单例对象的创建是分为三个步骤的:
createBeanInstance,其实第一步就是通过构造方法去进行实例化对象。但是这一步只是实例对象而已,并没有把对象的属性也给注入进去
然后这一步就是进行注入实例对象的属性,也就是从这步对spring xml中指定的property进行populate
最后一步其实是初始化XML中的init方法,来进行最终完成实例对象的创建。但是AfterPropertiesSet方法会发生循环依赖的步骤集中在第一步和第二步。
singletonObjects指单例对象的cache (一级缓存)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
singletonFactories指单例对象工厂的cache(三级缓存)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
earlySingletonObjects指提前曝光的单例对象的cache(二级缓存)
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
2. 然后是怎么具体使用到这个三级缓存的呢,或者说三级缓存的思路?
- 首先第一步是在Spring中会先去调用getSingleton(String beanName, boolean allowEarlyReference)来获取想要的单例对象。
- 然后第一步会先进行通过singletonObjects这个一级缓存的集合中去获取对象,如果没有获取成功的话并且使用isSingletonCurrentlyInCreation(beanName)去判断对应的单例对象是否正在创建中(也就是说当单例对象没有被初始化完全,走到初始化的第一步或者第二的时候),如果是正在创建中的话,会继续走到下一步
- 然后会去从earlySingletonObjects中继续获取这个对象,如果又没有获取到这个单例对象的话,并且通过参数传进来的allowEarlyReference标志,看是不是允许singletonFactories(三级缓存集合)去拿到该实例对象,如果allowEarlyReference为Ture的话,那么继续下一步
- 此时上一步中并没有从earlySingletonObjects二级缓存集合中拿到想要的实例对象,最后只能从三级缓存singletonFactories (单例工厂集合中)去获取实例对象,
- 然后把获取的对象通过Put(beanName, singletonObject)放到earlySingletonObjects(二级缓存中),然后在再从singletonFactories(三级缓存)对象中的集合中把该对象给remove(beanName)出去。
附上核心代码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
从一级缓存获取
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
从二级缓存获取
singletonObject = this.earlySingletonObjects.get(beanName);
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);}
3. 总结一下为什么这么做就能解决Spring中的循环依赖问题。
- 其实在没有真正创建出来一个实例对象的时候,这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来让大家认识,让大家使用
- A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,长大成人,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象也蜕变完美了!一切都是这么神奇!!
- 总结:Spring通过三级缓存加上“提前曝光”机制,配合Java的对象引用原理,比较完美地解决了某些情况下的循环依赖问题!
面试题2. Spring中bean的加载过程?
首先从大的几个核心步骤来去说明,因为Spring中的具体加载过程和用到的类实在是太多了。
- (1)、首先是先从AbstractBeanFactory中去调用doGetBean(name, requiredType, final Object[] args, boolean typeCheckOnly【这个是判断进行创建bean还是仅仅用来做类型检查】)方法,然后第一步要做的就是先去对传入的参数name进行做转换,因为有可能传进来的name=“&XXX”之类,需要去除&符号
- (2)、然后接着是去调用getSingleton()方法,其实在上一个面试题中已经提到了这个方法,这个方法就是利用“三级缓存” 来去避免循环依赖问题的出现的。【这里补充一下,只有在是单例的情况下才会去解决循环依赖问题】
- (3)、对从缓存中拿到的bean其实是最原始的bean,还未长大,所以这里还需要调用getObjectForBeanInstance(Object beanInstance, String name, String beanName, RootBeanDefinition mbd)方法去进行实例化。
- (4)、然后会解决单例情况下尝试去解决循环依赖,如果isPrototypeCurrentlyInCreation(beanName)返回为true的话,会继续下一步,否则throw new BeanCurrentlyInCreationException(beanName);
- (5)、因为第三步中缓存中如果没有数据的话,就直接去parentBeanFactory中去获取bean,然后判断containsBeanDefinition(beanName)中去检查已加载的XML文件中是否包含有这样的bean存在,不存在的话递归去getBean()获取,如果没有继续下一步
- (6)、这一步是吧存储在XML配置文件中的GernericBeanDifinition转换为RootBeanDifinition对象。这里主要进行一个转换,如果父类的bean不为空的话,会一并合并父类的属性
- (7)、这一步核心就是需要跟这个Bean有关的所有依赖的bean都要被加载进来,通过刚刚的那个RootBeanDifinition对象去拿到所有的beanName,然后通过registerDependentBean(dependsOnBean, beanName)注册bean的依赖
- (8)、然后这一步就是会根据我们在定义bean的作用域的时候定义的作用域是什么,然后进行判断在进行不同的策略进行创建(比如isSingleton、isPrototype)
- (9)、这个是最后一步的类型装换,会去检查根据需要的类型是否符合bean的实际类型去做一个类型转换。Spring中提供了许多的类型转换器
面试题3. Spring中bean的生命周期?
- 首先会先进行实例化bean对象
- 然后是进行对bean的一个属性进行设置
- 接着是对BeanNameAware(其实就是为了让Spring容器来获取bean的名称)、BeanFactoryAware(让bean的BeanFactory调用容器的服务)、ApplicationContextAware(让bean当前的applicationContext可以来取调用Spring容器的服务)
- 然后是实现BeanPostProcessor 这个接口中的两个方法,主要是对调用接口的前置初始化postProcessBeforeInitialization
- 这里是主要是对xml中自己定义的初始化方法 init-method = “xxxx”进行调用
- 然后是继续对BeanPostProcessor 这个接口中的后置初始化方法进行一个调用postProcessAfterInitialization()
- 其实到这一步,基本上这个bean的初始化基本已经完成,就处于就绪状态
- 然后就是当Spring容器中如果使用完毕的话,就会调用destory()方法
- 最后会去执行我们自己定义的销毁方法来进行销毁,然后结束生命周期
- 补充:当在执行第六步之后,如果Spring初始的这个bean的作用域是Prototype的话,之后的过程就不归Spring来去管理了,直接就交给用户就可以了。
面试题4. 说一下Spring中的IOC核心思想和DI?
之前自己有总结过的一篇:https://blog.csdn.net/qq_36520235/article/details/79383238
面试题5. 说说Spring中的几种事务和隔离级别?
面试题6. 说一下SpringMVC中的拦截器和Servlet中的filter有什么区别?
首先最核心的一点他们的拦截侧重点是不同的,SpringMVC中的拦截器是依赖JDK的反射实现的,SpringMVC的拦截器主要是进行拦截请求,通过对Handler进行处理的时候进行拦截,先声明的拦截器中的preHandle方法会先执行,然而它的postHandle方法(他是介于处理完业务之后和返回结果之前)和afterCompletion方法却会后执行。并且Spring的拦截器是按照配置的先后顺序进行拦截的。
而Servlet的filter是基于函数回调实现的过滤器,Filter主要是针对URL地址做一个编码的事情、过滤掉没用的参数、安全校验(比较泛的,比如登录不登录之类)
面试题7. spring容器的bean什么时候被实例化?
(1)如果你使用BeanFactory作为Spring Bean的工厂类,则所有的bean都是在第一次使用该Bean的时候实例化
(2)如果你使用ApplicationContext作为Spring Bean的工厂类,则又分为以下几种情况:
如果bean的scope是singleton的,并且lazy-init为false(默认是false,所以可以不用设置),则 ApplicationContext启动的时候就实例化该Bean,并且将实例化的Bean放在一个map结构的缓存中,下次再使 用该 Bean的时候,直接从这个缓存中取
如果bean的scope是singleton的,并且lazy-init为true,则该Bean的实例化是在第一次使用该Bean的时候进 行实例化
如果bean的scope是prototype的,则该Bean的实例化是在第一次使用该Bean的时候进行实例化
面试题8.说一下Spring中AOP的底层是怎么实现的?,再说说一下动态代理和cglib区别?
Spring中AOP底层的实现其实是基于JDK的动态代理和cglib动态创建类进行动态代理来实现的:
1. 第一种基于JDK的动态代理的原理是:
需要用到的几个关键成员
InvocationHandler (你想要通过动态代理生成的对象都必须实现这个接口)
真实的需要代理的对象(帮你代理的对象)
Proxy对象(是JDK中java.lang.reflect包下的)
下面是具体如何动态利用这三个组件生成代理对象
(1)首先你的真是要代理的对象必须要实现InvocationHandler 这个接口,并且覆盖这个接口的invoke(Object proxyObject, Method method, Object[] args)方法,这个Invoker中方法的参数的proxyObject就是你要代理的真实目标对象,方法调用会被转发到该类的invoke()方法, method是真实对象中调用方法的Method类,Object[] args是真实对象中调用方法的参数
(2)然后通过Proxy类去调用newProxyInstance(classLoader, interfaces, handler)方法,classLoader是指真实代理对象的类加载器,interfaces是指真实代理对象需要实现的接口,还可以同时指定多个接口,handler方法调用的实际处理者(其实就是帮你代理的那个对象),代理对象的方法调用都会转发到这里,然后直接就能生成你想要的对象类了。
下面是Proxy调用newProxyInstance的方法源码
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) throws IllegalArgumentException {
//验证传入的InvocationHandler不能为空
Objects.requireNonNull(h);
//复制代理类实现的所有接口
final Class<?>[] intfs = interfaces.clone();
//获取安全管理器
final SecurityManager sm = System.getSecurityManager();
//进行一些权限检验
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
//该方法先从缓存获取代理类, 如果没有再去生成一个代理类
Class<?> cl = getProxyClass0(loader, intfs);
try {
//进行一些权限检验
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
//获取参数类型是InvocationHandler.class的代理类构造器
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
//如果代理类是不可访问的, 就使用特权将它的构造器设置为可访问
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
//传入InvocationHandler实例去构造一个代理类的实例
//所有代理类都继承自Proxy, 因此这里会调用Proxy的构造器将InvocationHandler引用传入
return cons.newInstance(new Object[]{h});
} catch (Exception e) {
//为了节省篇幅, 笔者统一用Exception捕获了所有异常
throw new InternalError(e.toString(), e);
}
}
大佬的博客借鉴一下; https://www.cnblogs.com/liuyun1995/p/8157098.html
面试题9. Spring的几种注入方式说一下?
(1)构造方法注入:
<!-- 注册userService -->
<bean id="userService" class="com.lyu.spring.service.impl.UserService">
<constructor-arg ref="userDaoJdbc"></constructor-arg>
</bean>
<!-- 注册jdbc实现的dao -->
<bean id="userDaoJdbc" class="com.lyu.spring.dao.impl.UserDaoJdbc"></bean>
注意的点:
如果有多个构造参数,那么与构造方法参数列表参数的顺序无关
如果有多个构造方法且参数顺序不同,那么会按第一个构造方法进行注入
(2)set方法注入:
<!-- 注册userService -->
<bean id="userService" class="com.lyu.spring.service.impl.UserService">
<!-- 写法一 -->
<!-- <property name="UserDao" ref="userDaoMyBatis"></property> -->
<!-- 写法二 -->
<property name="userDao" ref="userDaoMyBatis"></property>
</bean>
<!-- 注册mybatis实现的dao -->
<bean id="userDaoMyBatis" class="com.lyu.spring.dao.impl.UserDaoMyBatis"></bean>
注意的点:
其实set方法的注入原理就是,spring会将name值的每个单词首字母转换成大写,然后再在前面拼接上"set"构成一个方法名,然后去对应的类中查找该方法,通过反射调用,实现注入
如果通过set方法注入属性,那么spring会通过默认的空参构造方法来实例化对象,所以如果在类中写了一个带有参数的构造方法,一定要把空参数的构造方法写上,否则spring没有办法实例化对象,导致报错。
(3)注解的方式注入:
@Resource:java的注解,默认以byName的方式去匹配与属性名相同的bean的id,如果没有找到就会以byType的方式查找,如果byType查找到多个的话,使用@Qualifier注解(spring注解)指定某个具体名称的bean
@Autowired:spring注解,默认是以byType的方式去匹配类型相同的bean,如果只匹配到一个,那么就直接注入该bean,无论要注入的 bean 的 name 是什么;如果匹配到多个,就会调用 DefaultListableBeanFactory 的 determineAutowireCandidate 方法来决定具体注入哪个bean。determineAutowireCandidate 方法的内容如下:
// candidateBeans 为上一步通过类型匹配到的多个bean,该 Map 中至少有两个元素。
protected String determineAutowireCandidate(Map<String, Object> candidateBeans, DependencyDescriptor descriptor) {
// requiredType 为匹配到的接口的类型
Class<?> requiredType = descriptor.getDependencyType();
// 1. 先找 Bean 上有@Primary 注解的,有则直接返回
String primaryCandidate = this.determinePrimaryCandidate(candidateBeans, requiredType);
if (primaryCandidate != null) {
return primaryCandidate;
} else {
// 2.再找 Bean 上有 @Order,@PriorityOrder 注解的,有则返回
String priorityCandidate = this.determineHighestPriorityCandidate(candidateBeans, requiredType);
if (priorityCandidate != null) {
return priorityCandidate;
} else {
Iterator var6 = candidateBeans.entrySet().iterator();
String candidateBeanName;
Object beanInstance;
do {
if (!var6.hasNext()) {
return null;
}
// 3. 再找 bean 的名称匹配的
Entry<String, Object> entry = (Entry)var6.next();
candidateBeanName = (String)entry.getKey();
beanInstance = entry.getValue();
} while(!this.resolvableDependencies.values().contains(beanInstance) && !this.matchesBeanName(candidateBeanName, descriptor.getDependencyName()));
return candidateBeanName;
}
}
}