本文主要举一些常见的关于Spring的经典面试题,看看我们对Spring了解有多深。
别的话就不多说了直接开整吧。(本文的解释参考官方和一些博客,描述不清请见谅!)
IoC控制反转
IoC(Inverse of Control):是一种设计思想(模式),意思就是把原本在程序中需要我们手动创建对象的过程交给 Spring 来处理。
给出一个简单的例子:
public interface MessageService {
String getMessage();
}
public class EmailService implements MessageService {
@Override
public String getMessage() {
return "Email message";
}
}
public class MyApplication {
private MessageService messageService;
// 通过构造函数注入依赖
public MyApplication(MessageService messageService) {
this.messageService = messageService;
}
public void processMessages() {
System.out.println(messageService.getMessage());
}
}
public class Main {
public static void main(String[] args) {
MessageService emailService = new EmailService();
MyApplication app = new MyApplication(emailService);
app.processMessages();
}
}
在Java中,IoC通常与依赖注入(DI)一起使用,而在Python中,IoC的实现通常更加轻量级,并且内置在语言中。在C#中,IoC通常与.NET框架一起使用。
DI(Dependancy Injection):依赖注入,站在容器的角度,将对象创建依赖的其它对象注入到对象中。有助于降低对象之间的耦合度,并使代码更易于测试和维护。
还是一个例子:
public interface MessageService {
String getMessage();
}
public class EmailService implements MessageService {
@Override
public String getMessage() {
return "Email message";
}
}
public class MyApplication {
private MessageService messageService;
// 通过构造函数注入依赖
public MyApplication(MessageService messageService) {
this.messageService = messageService;
}
public void processMessages() {
System.out.println(messageService.getMessage());
}
}
public class Main {
public static void main(String[] args) {
MessageService emailService = new EmailService();
MyApplication app = new MyApplication(emailService);
app.processMessages();
}
}
直接来八股吧,直接背把xdm
IoC 容器是 Spring⽤来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注⼊。这样可以很⼤程度上简化应⽤的开发,把应⽤从复杂的依赖关系中解放出来。IoC 容器就像是⼀个⼯⼚⼀样,当我们需要创建⼀个对象的时候,只需要配置好配置⽂件/注解即可,完全不⽤考虑对象是如何被创建出来的。
AOP 动态代理
AOP(Aspect Oriented Programming):面向切面编程,将与业务无关,但却被一些业务模块共同调用的逻辑或责任(事物、日志和权限控制等)分装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
AOP通过提供另一种思考程序结构的方式,对面向对象编程(OOP)进行补充。在OOP中,模块化的关键单元是类,而在AOP中,模块化的单元是切面(aspect)。Spring框架中的AOP模块提供了声明式企业服务和自定义切面的能力,从而增强了OOP的功能。
Spring框架中提供了两种主要的自定义切面的方法:基于模式的方法和基于@AspectJ注解的方法。
基于模式的方法:
- 基于模式的方法是通过在Spring配置文件中定义切面和通知的方式来实现的。这种方法需要在XML配置文件中声明切面和通知,然后将它们与目标bean进行关联。
- 通知可以是“前置通知”、“后置通知”、“环绕通知”、“异常通知”和“最终通知”等。每种通知都可以在目标方法的不同执行阶段被触发。
- 通过基于模式的方法,可以更加灵活地控制切面和通知的织入方式,但是需要在XML配置文件中进行繁琐的声明。
基于@AspectJ注解的方法:
- 基于@AspectJ注解的方法是通过在普通的Java类中使用注解来定义切面和通知的方式来实现的。这种方法更加简洁和直观,使得切面的定义更加集中和易于维护。
- 通过@AspectJ注解,可以在普通的Java类中定义切面和通知,而无需在XML配置文件中进行繁琐的声明。
- 通知的类型和触发时机与基于模式的方法相同,包括“前置通知”、“后置通知”、“环绕通知”、“异常通知”和“最终通知”等。
这里就给出通过注解的一个简单的实现:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeServiceMethods() {
System.out.println("Before executing service method");
}
}
定义了一个名为LoggingAspect
的切面类,并使用了 @Aspect
注解来标识它是一个切面。在切面类中,使用了@Before
注解来定义一个前置通知,它会在执行com.example.service
包中的所有方法之前被触发。beforeServiceMethods
方法会在目标方法执行之前输出日志信息。
Spring AOP使用动态代理,如果要代理的对象实现了接口,Spring AOP会使用JDK动态代理来创建代理对象。而对于没有实现接口的对象,Spring AOP会使用基于ASM框架的字节码的Cglib动态代理,生成一个被代理对象的子类来作为代理。荔枝如下:
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibProxyExample {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyClass.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before method " + method.getName());
Object result = proxy.invokeSuper(obj, args);
System.out.println("After method " + method.getName());
return result;
}
});
MyClass proxy = (MyClass) enhancer.create();
proxy.myMethod();
}
static class MyClass {
public void myMethod() {
System.out.println("Executing myMethod");
}
}
}
Bean 的生命周期
单例:singleton 生命周期和容器相同
多例:prototype
出生:使用对象时 Spring 框架来创建
活着:对象只要在使用的过程中就一直是活着的
死亡:当对象长时间不用且没有其它的对象引用该对象时,会有 JAVA 的垃圾回收机制回收
下图是Bean生命周期的过程:
什么!不看英文的吗?哦,好的,换个简单简单点的~~~~
IoC容器初始化加载Bean的流程(具体的可自行了解)
加载配置文件:
从XML文件或注解配置中获取Bean的定义和依赖关系
创建Bean实例:
根据配置文件中的定义,实例化对象并设置属性
注入依赖:
确保各个Bean之间能够正确地相互调用和协作
调用初始化方法:
如果Bean定义中指定了初始化方法,调用这些方法进行额外的初始化操作
将Bean注册到容器:
将创建好的Bean实例注册到容器中,以便后续的使用
四个阶段
- 实例化 Instantiation
- 属性赋值 Populate
- 初始化 Initialization
- 销毁 Destruction
多个扩展点
- 影响多个 Bean (BeanPostProcessor、InstantiationAwareBeanPostProcessor)
- 影响单个 Bean (Aware)
完整的流程
- 实例化 Bean -- new 个对象
- 按照 Spring 上下文对实例化的 Bean 进行配置 -- IoC注入
- 如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String)方法,也就是根据就是Spring配置文件中Bean的id和name进行传递
-
如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现setBeanFactory(BeanFactory)也就是Spring配置文件配置的Spring工厂自身进行传递;
-
如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,和4传递的信息一样但是因为ApplicationContext是BeanFactory的子接口,所以更加灵活
-
如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization()方法,BeanPostProcessor经常被用作是Bean内容的更改,由于这个是在Bean初始化结束时调用那个的方法,也可以被应用于内存或缓存技术
-
如果Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法。
-
如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessAfterInitialization(),打印日志或者三级缓存技术里面的bean升级
-
以上工作完成以后就可以应用这个Bean了,那这个Bean是一个Singleton的,所以一般情况下我们调用同一个id的Bean会是在内容地址相同的实例,当然在Spring配置文件中也可以配置非Singleton,这里我们不做赘述
-
当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,或者根据spring配置的destroy-method属性,调用实现的destroy()方法
这段是借鉴的,感觉写的比我写的“荔枝”多了,很好!
Bean 的作用域
- singleton:IoC容器中只有唯一的Bean实例。Spring 中的 Bean 默认都是单例的
- prototype:每次获取都会创建一个新的bean实例。
- request:(web页面可用)每次Http请求都会产生一个新的bean(请求bean),在当前的Http request有效
- session:(web页面可用)每次新来一个session的Http请求都会生成一个新的bean(会话bean),在当前的Http session 内有效
- application/global-session:(仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效
- websocket:每一次 WebSocket 会话产生一个新的 bean
配置bean的作用域有两种方式:
xml方式:
<bean id="..." class="..." scope="singleton"></bean>
注解:
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person personPrototype() {
return new Person();
}
拐个弯,扯一点其它的!!!
默认的是 singleton的,多个线程同时访问同一个bean可能会存在线程安全的问题
那么如何解决这个问题呢?
-
在Bean对象中尽量避免定义可变的成员变量(不太现实)
-
在类中定义⼀个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中
简单提一嘴ThreadLocal,后续会详细讲讲!
每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
好了,拐回来了,扯远了,这里又涉及到了Bean的线程安全问题,就不在多说了。
循环依赖
循环依赖其实就是循环引用,也就是两个或者两个以上的 Bean 互相持有对方,最终形成闭环。比如A 依赖于B,B又依赖于A
Spring中循环依赖场景有:
-
prototype 原型 bean循环依赖
-
构造器的循环依赖(构造器注入)
-
Field 属性的循环依赖(set注入)
其中,构造器的循环依赖问题无法解决,在解决属性循环依赖时,可以使用懒加载,spring采用的是提前暴露对象的方法。
懒加载@Lazy解决循环依赖问题
Spring 启动的时候会把所有bean信息(包括XML和注解)解析转化成Spring能够识别的BeanDefinition并存到Hashmap里供下面的初始化时用,然后对每个 BeanDefinition 进行处理。普通 Bean 的初始化是在容器启动初始化阶段执行的,而被lazy-init=true修饰的 bean 则是在从容器里第一次进行context.getBean() 时进行触发。
三级缓存解决循环依赖问题
-
Spring容器初始化ClassA通过构造器初始化对象后提前暴露到Spring容器中的singletonFactorys(三级缓存中)。
-
ClassA调用setClassB方法,Spring首先尝试从容器中获取ClassB,此时ClassB不存在Spring 容器中。
-
Spring容器初始化ClassB,ClasssB首先将自己暴露在三级缓存中,然后从Spring容器一级、二级、三级缓存中一次中获取ClassA 。
-
获取到ClassA后将自己实例化放入单例池中,实例 ClassA通过Spring容器获取到ClassB,完成了自己对象初始化操作。
-
这样ClassA和ClassB都完成了对象初始化操作,从而解决了循环依赖问题。
不想搞了,放到下一篇吧,脑阔疼!!散会!!