以下问题是本人以及同事面试中遇到的一些问题,记录分享给大家,希望每个人都能拿到自己期望的offer
1 spring bean的生命周期
为了方便大家理解 这里提供两张图,第一张是粗略图 第二张比较详细。大家根据各自的情况选择
这里针对第二张图进行详细解析(一般来说先给面试官解释第一张图,第二张给面试官留坑,等他详细问)
1.1 第一步:定义一个类,该类必须实现BeanNameAware接口
public class User implements BeanNameAware
{
public void setBeanName(String name)
{
System.out.println("bean的名称是:" + name);
}
}
1.2 第二步:在Spring的配置文件中管理Bean
<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="user" class="com.qf.bean.User"></bean>
</beans>
1.3 第三步:编写测试类
public class TestUser {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
}
}
1.4 第四步:打断点测试
我们在User的setBeanName上打上断点,查看一下效果:
以上结果就验证了上边所说的,这个方法里面会接收Bean的id的值。
1.5 BeanClassLoaderAware
在源码中,会将其描述成一个接口。
这个接口里面定义了setBeanClassLoader方法。这个方法就是设置Bean加载器的方法,方法的形式参数传递的是一个类加载器对象。
1.6 BeanFactoryAware
Spring也是将其描述成了一个接口。
如果一个Bean实现了这个接口,可以获取这个Bean的Bean工厂。
1.7 EnvironmentAware
如果一个Bean实现了这个接口,就可以获取当前Bean对应的运行环境。
如果我们想获取Bean对应的环境信息,我们可以这么做:
public class User implements EnvironmentAware{
public void setEnvironment(Environment environment) {
System.out.println(environment);
}
}
1.8 ResourceLoaderAware
这也是一个接口。
如果一个Bean实现了这个接口我们可以获得一个资源加载器,用来去加载Bean所需要用到的资源信息。
1.9 ApplicationEventPublisherAware
这个接口是用来做事件发布用的。Spring设计这个组件主要是用来组件解耦使用的,如果一个Bean所属的类实现了这个接口,就可以获取一个事件发布器。
1.10 MessageSourceAware
Spring同样将其设计成了一个接口,我们可以通过这个接口去获取Bean相关的国际化资源信息。
1.11 ApplicationContextAware
当一个Bean所属的类实现了这个接口之后,这个类就可以方便地获得ApplicationContext对象(Spring上下文),Spring发现某个Bean实现了ApplicationContextAware接口,Spring容器会在创建该Bean之后,自动调用该Bean的setApplicationContext(参数)方法,调用该方法时,会将容器本身ApplicationContext对象作为参数传递给该方法。
我们在Bean所属的类上面实现这个接口:
public class User implements ApplicationContextAware {
private ApplicationContext context;
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
}
我们打断点验证我们的猜想:
我们看看这个context是什么:
这不就是我们上面说的,将ApplicationContext实例对象传入进来了吗?
1.12 ServletContextAware
在上面我们获取了Spring的Bean工厂实例对象,接下来Spring框架会去判断当前容器是否是一个Web类型的容器实例,如何判断?就需要调用ServletContextAware中的setServletContext方法,获取一个ServletContext容器。那到底是如何获取的呢?
private ServletContext servletContext;
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
1.13 BeanPostProcessor
该接口我们也叫Bean的后置处理器,作用是在Bean对象在实例化和依赖注入完毕后,在显式调用初始化方法的前后添加我们自己自定义的逻辑。注意是Bean实例化完毕后及依赖注入完成后触发的。接口的源码如下
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
这两个方法是什么意思呢,文哥给大家详细介绍:
- postProcessBeforeInitialization:Bean实例化、依赖注入完毕之后,在调用Bean初始化之前完成一些定制的初始化任务
- postProcessAfterInitialization:Bean实例化、依赖注入、初始化完毕时执行
现在,我们就自定义一个Bean的后置处理器,有以下几个实现步骤。
1.14 第一步:创建一个类,实现BeanPostProcessor接口
public class CustomizeBeanProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("执行before方法,bean是:" + bean + "bean的名称是:" + beanName);
return bean;
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("执行after方法,bean是:" + bean + "bean的名称是:" + beanName);
return bean;
}
}
在这里特别提示:这两个方法的返回值千万不能返回为null。如果返回null那么在后续初始化方法因为通过getBean()方法获取不到Bean实例对象会报空指针异常,因为后置处理器从Spring IoC容器中取出Bean实例对象没有再次放回IoC容器中。
1.15 第二步:定义一个Pojo类
我们定义一个Student类,在里面定义一个init方法。
public class Student {
private Integer id;
private String name;
private Integer age;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
//定义一个初始化bean的时候需要执行的方法
public void init(){
System.out.println("初始化方法init执行了.....");
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
1.16 第三步:在Spring的配置文件中配置pojo和Bean的后置处理器
<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--管理student-->
<bean id="student" class="com.qf.bean.Student" init-method="init">
<property name="id" value="10010"></property>
<property name="name" value="eric"></property>
<property name="age" value="12"></property>
</bean>
<!--管理bean的后置处理器-->
<bean class="com.qf.processor.CustomizeBeanProcessor"></bean>
</beans
1.17 第四步:测试
我们定义一个测试类,初始化一个IOC容器,然后从容器中获取Bean,然后查看控制台效果。
public class TestStudent {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Student student = (Student) context.getBean("student");
System.out.println(student);
}
}
我们查看控制台输出信息:
通过验证,我们终于知道了Bean的后置处理器的作用了。
1.18 InitializingBean
该接口只有这一个接口。这个方法将在所有的属性被初始化后调用,但是会在 init 前调用。这个接口我们一般在工作中用于工厂+策略模式优化过多的if else代码块。
现在给大家演示一下,这个接口的简单用法:
1.19 第一步:在pojo类上实现这个接口。
1.20 第二步:运行测试代码,查看控制台效果
到这里我们基本上给大家把Spring Bean生命周期中所涉及的主要接口给大家讲解清楚了
2 spring循环依赖怎么解决?
老规矩 先上图
2.1 一级缓存和二级缓存
总:什么是循环依赖问题,A依赖B,B依赖A
分:先说明bean的创建过程:实例化,初始化(填充属性)
1、先创建A对象,实例化A对象,此时A对象中的b属性为空,填充属性b
2、从容器中查找B对象,如果找到了,直接赋值不存在循环依赖问题(不通),找不到直接创建B对象
3、实例化B对象,此时B对象中的a属性为空,填充属性a
4、从容器中查找A对象,找不到,直接创建
形成闭环的原因
此时,如果仔细琢磨的话,会发现A对象是存在的,只不过此时的A对象不是一个完整的状态,只完成了实例化但是未完成初始化,如果在程序调用过程中,拥有了某个对象的引用,能否在后期给他完成赋值操作,可以优先把非完整状态的对象优先赋值,等待后续操作来完成赋值,相当于提前暴露了某个不完整对象的引用,所以解决问题的核心在于实例化和初始化分开操作,这也是解决循环依赖问题的关键,
当所有的对象都完成实例化和初始化操作之后,还要把完整对象放到容器中,此时在容器中存在对象的几个状态,完成实例化=但未完成初始化,完整状态,因为都在容器中,所以要使用不同的map结构来进行存储,此时就有了一级缓存和二级缓存,如果一级缓存中有了,那么二级缓存中就不会存在同名的对象,因为他们的査找顺序是1,2,3这样的方式来查找的。一级缓存中放的是完整对象,二级缓存中放的是非完整对象
2.2 到此 二级缓存的答案已经有了 那么为什么要使用第三级缓存呢?
对此 先理解三个小问题
2.2.1 在创建代理对象的时候是否需要创建原始对象?
需要
2.2.2 容器中能否同时存在两个同名的不同对象?
不能
2.2.3 如果创建出了代理对象,那么原始对象应该怎么处理?
当创建出代理对象之后,需要将代理对象覆盖原始对象
2.2.4 那么为什么要引入三级缓存呢?为什么要传入一个lambda表达式呢?
正常的bean的生命周期是先通过createBeanlnstance创建出原始对象,然后在populateBean的方法中完成对象属性的赋值工作,然后在BeanPostProcessor的后置处理方法中完成代理对象的创建工作,也就是说按照正常的执行逻辑,是完成属性的赋值之后才会创建出代理对象,那么意味着最后创建出的是代理对象,但是赋值的时候赋的是原始对象,所以会出现一个错误:that said other beans do not use the final version of the bean. 当引入lambda表达式之后相当于将生成代理对象的过程给提前了,也就是说在完成对象的属性赋值的时候必须要唯一性的确定好我需要的到底是代理对象还是原始对象,参考 (getEarlyBeanReference方法)也就是说我们在赋值的前一刻必须要确定好最终的结果,但是又因为我们没有办法确定什么时刻会给什么对象的属性赋值,所以采用lambda表达式的方法延迟执行,只有在对象赋值的最后一刻才确定出到底是什么对象。
3 beanFactor和factorBean有什么区别?
相同点:都是用来创建bean对象的、
不同点:使用BeanFactory创建对象的时候,必须要遵循严格的生命周期流程,太复杂了,,如果想要简单的自定义某个对象的创建,同时创建完成的对象想交给spring来管理,那么就需要实现FactroyBean接口了
4 Spring AOP底层实现原理
4.1 首先创建代理对象 通过creatProxy方法
接下来深入看一下这个方法做了什么(此处只说明重点)
1、创建代理工厂
2、将当前对象的属性值复制到代理工厂中
3、判断一下是jdk动态代理还是cglib代理(未完成判断 设置了一些属性值)
4、构建advisors对象数组(所有的通知)
5、设置要代理的类
6、创建真正的代理对象 (重点部分开始)
7、具体判断使用jdk 还是cglib 代理 (createAoproxy 判断使用什么方式 getProxy 具体创建代理对象)
8、决定使用什么方式后开始创建代理对象 (以cglib为例)
1、代理对象的创建过程(advice,切面,切点)
2、通过idk或者cglib的方式来生成代理对象
3、在执行方法调用的时候,会调用到生成的字节码文件中,直接回找到DynamicAdvisoredlnterceptor类中的intercept方法,从此方法开始执行
4、根据之前定义好的通知来生成拦截器链
5、从拦截器链中依次获取每一个通知开始进行执行,在执行过程中,为了方便找到下一个通知是哪个,会有一个CglibMethodInvocation的对象,找的时候是从-1的位置一次开始查找并且执行的。
5 Spring 事务原理
总:
spring的事务是由aop来实现的,首先要生成具体的代理对象,然后按照aop的整套流程来执行具体的操作逻辑,正常情况下要而讨涌知来完成核心功能,但是事务不是通过通知来实现的,而是通过一个Transactioninterceptor来实现的,然后调用invoke来实现具体的逻辑
分:
1、先做准备工作,解析各个方法上事务相关的属性,根据具体的属性来判断是否开始新事务
2、当需要开启的时候,获取数据库连接,关闭自动提交功能,开起事务
3、执行具体的sql逻辑操作
4、在操作过程中,如果执行失败了,那么会通过completeTransactionAfterThrowing看来完,成事务的回滚操作,回滚的具体逻辑是通过doRollBack方法来实现的,实现的时候也是要先获取连接对象,通过连接对象来回滚
5、如果执行过程中,没有任何意外情况的发生,那么通过commitTransactionAfterReturning来完成事务的提交操作,提交的具体逻辑是通过doCommit方法来实现的,实现的时候也是要获取连接,通过连接对象来提交
6、当事务执行完毕之后需要清除相关的事务信息cleanupTransactionInfo
6 Spring 把bean 注入到IOC的方式
6.1. 使用xml的方式来声明Bean的定义,Spring容器在启动的时候会加载并解析这个xml,把bean装载到ioc容器中。
6.2. 使用@ComponentScan注解来扫描声明了@Controller、@Service、@Repository、@Component注解的类。
6.3. 使用@Configuration注解声明配置类,并使用@Bean注解实现Bean的定义,这种方式其实是xml配置方式的一种演变。
6.4. 使用@Import注解,导入配置类或者普通的Bean。
6.5. 使用FactoryBean工厂bean,动态构建一个Bean实例,Spring Cloud OpenFeign里面的动态代理实例就是使用FactoryBean来实现的。
6.6. 实现ImportBeanDefinitionRegistrar接口,可以动态注入Bean实例。这个在spring boot里面的启动注解有用到。
6.7. 实现ImportSelector接口,动态批量注入配置类或者Bean对象,这个在spring boot里面的自动装配机制里面有用到。