文章目录
- 一、IoC
- 二、AOP
- 三、bean
- 3.1 bean 生命周期
- 3.2 三种依赖注入方式
- 3.3 bean 线程安全
- 四、SpringMVC
- 五、常用注解
- 5.1 @Scope
- 5.2 @PostConstruct 和 @PreDestroy
- 5.3 @Component 和 @Bean
- 5.4 @Autowired 和 @Resource
- 六、基于 ApplicationContextAware 实现工厂模式
- 七、事务失效
- 八、三级缓存与循环依赖
一、IoC
IoC(Inversion of Control)控制反转:控制反转是一种设计思想,它将组件的创建和管理交给容器,从而降低组件之间的耦合度。控制是指实例化以及管理对象的权力,反转是指将控制权交给 IoC 容器。IoC 容器负责对象的创建、初始化等一系列工作,被创建或被管理的对象在 IoC 容器中统称为 bean。
二、AOP
面向切面编程的核心思想就是将横切关注点从核心业务逻辑中分离出来,形成一个个的切面。横切关注点指的是一些分散在多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等操作),如果我们在每个类或对象中都重复实现这些行为,那么会导致代码的冗余、复杂和难以维护。
Spring AOP 基于动态代理实现。当目标对象实现了某个接口时,Spring AOP 会使用 JDK 动态代理来生成对应接口的代理对象。对于没有实现接口的对象,Spring AOP 会使用 CGLIB 生成一个目标对象的子类作为代理对象。在运行时,代理类会替代原始目标类来执行方法调用,而代理类中的功能是基于切面类定义和实现的。
AOP 中的核心概念:
- 目标对象:被代理或操作的对象。
- 连接点:目标对象中定义的所有可被 AOP 控制的方法均为连接点。
- 切入点:被实际增强的连接点,具体范围由切入点表达式控制。
- 通知:拦截到连接点之后要执行的增强逻辑,比如一些共性功能。
- 切面:切入点 + 通知。
切入点表达式用来描述切入点方法的表达式,主要用来决定目标对象中的哪些连接点需要加入通知,主要包括使用 execution()
根据方法的签名来匹配和使用 @annotation()
根据注解匹配。
三、bean
3.1 bean 生命周期
- 初始化容器
- 通过反射实例化 bean,创建对象。
- 执行构造方法。
- 如果实现了
Aware
相关依赖,如ApplicationContextAware
,执行对应方法。 - 属性赋值与依赖注入,解决循环依赖。
- 执行
@PostConstruct
注解标识的方法。 - 如果项目中实现了
BeanPostProcessor
接口,将当前类作为参数执行自定义的postProcessBeforeInitialization
方法。 - 如果实现了
InitializingBean
接口,执行afterPropertiesSet
方法。 - 如果配置了自定义的 init-method,即
@Bean(initMethod = "")
,执行对应方法。 - 如果项目中实现了
BeanPostProcessor
接口,将当前类作为参数执行自定义的postProcessAfterInitialization
方法。
- 获取并使用 bean
- 销毁容器
- 如果配置了自定义的 destroy-method,即
@Bean(destroyMethod = "")
,执行对应方法。 - 如果实现了
DisposableBean
接口,执行destory()
方法。
- 如果配置了自定义的 destroy-method,即
3.2 三种依赖注入方式
- 字段注入:实现简单,但是存在注入对象不能用
final
修饰、难以进行单元测试等问题,因此并不推荐使用字段注入。 - 构造器注入:唯一一个注入对象可以使用
final
修饰的注入方法,同时能够检测循环依赖。但是,当一个类有很多依赖项时构造函数的参数列表可能会变得很长。此外,如果一个类有可选的依赖项,可能需要创建多个构造函数重载,灵活性较低。 - setter 方法注入:灵活性较高,不过注入对象也不能用
final
修饰。
总的使用原则是:强制的依赖就用构造器注入,可选、可变的依赖就用 setter 注入。
3.3 bean 线程安全
bean 是否线程安全,取决于其作用域和状态。
以最常用的两种作用域 prototype
和 singleton
为例:
prototype
作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。singleton
作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题,具体要看 bean 是否有状态,如果是无状态 bean,那就不存在线程安全问题,如果是有状态 bean,那就存在线程安全问题。这里的有无状态指的是对于成员变量,除了查询以外,是否还会对其进行修改。
对于有状态单例 bean 的线程安全问题,有以下几种解决办法:
- 在 bean 中尽量避免定义可变的成员变量。
- 通过
ThreadLocal
或互斥锁控制成员变量的修改和访问。 - 采用
prototype
作用域。
四、SpringMVC
SpringMVC 技术与 Servlet 技术功能等同,均属于表现层开发技术。
五、常用注解
5.1 @Scope
声明 bean 的作用域,常见的有以下四种:
- singleton:Spring 中默认的作用域,IoC 容器中只有唯一的 bean 实例,这意味着所有客户端共享相同的 bean 实例。
- prototype:每次获取都会创建一个新的 bean 实例,这意味着每个客户端都拥有自己的 bean 实例。
- request:bean 的生命周期与 HTTP 请求的生命周期相对应,每个 HTTP 请求都会创建一个新的 bean 实例,该 bean 仅在该请求内可见。
- session:每一次来自新 session 的 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 session 内有效。
使用示例:
@Component
@Scope("singleton")
public class Solution {
}
5.2 @PostConstruct 和 @PreDestroy
@PostConstruct
和 @PreDestroy
并非由 Spring 提供,而是 Java 自带的注解。@PostConstruct
会在依赖注入完成后被自动调用,并且只会被调用一次,用于完成一些初始化操作。而 @PreDestroy
则会在容器销毁 bean 的时候回调执行,用于完成相关的销毁操作。
bean 初始化过程中的执行顺序为:
Constructor(构造方法)-> @Autowired(依赖注入)-> @PostConstruct(初始化方法)
使用示例:
@Component
public class Solution {
@PostConstruct
void init() {
System.out.println("init...");
}
@PreDestroy
void destroy() {
System.out.println("destroy...");
}
}
5.3 @Component 和 @Bean
@Component
注解作用于类,标识这是一个 bean,而@Bean
注解作用于方法,通常方法体中包含产生 bean 的逻辑。@Component
通常是通过类路径扫描来自动侦测 bean 并装配到 Spring 容器中,可以使用@ComponentScan
定义要扫描的路径,而@Bean
通常指定了这个方法的返回值将被注册为一个 bean。@Bean
注解比@Component
注解的自定义性更强,可以在方法内部实现更复杂的bean
创建和初始化逻辑,同时第三方依赖中的 bean 只能通过@Bean
声明(因为第三方的 bean 是只读的,没法加@Component
注解)。
5.4 @Autowired 和 @Resource
@Autowired
是 Spring 提供的注解,@Resource
是 JDK 提供的注解。@Autowired
默认的注入方式是根据类型匹配,@Resource
默认的注入方式是根据名称匹配。- 当一个接口存在多个实现类的情况下,
@Autowired
和@Resource
除了都能够通过名称匹配到对应的 bean 以外,@Autowired
还可以通过@Qualifier
注解来显式指定,@Resource
则可以通过 name 属性来显式指定。 @Autowired
支持在构造函数、方法、字段和参数上使用,@Resource
主要用于字段和方法上的注入,不支持在构造函数或参数上使用。
六、基于 ApplicationContextAware 实现工厂模式
抽象产品:
package atreus.ink.log;
import org.springframework.stereotype.Component;
@Component
public abstract class AbstractLog {
protected abstract String getName();
protected void doLog() {
System.out.println("atreus.ink.log.AbstractLog#doLog");
}
}
具体产品:
package atreus.ink.log;
import org.springframework.stereotype.Component;
@Component
public class JavaLog extends AbstractLog {
@Override
public String getName() {
return "JavaLog";
}
@Override
public void doLog() {
super.doLog();
System.out.println("atreus.ink.log.JavaLog#doLog");
}
}
package atreus.ink.log;
import org.springframework.stereotype.Component;
@Component
public class CppLog extends AbstractLog {
@Override
public String getName() {
return "CppLog";
}
@Override
public void doLog() {
super.doLog();
System.out.println("atreus.ink.log.CppLog#doLog");
}
}
工厂:
package atreus.ink.log;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class LogFactory implements ApplicationContextAware {
private static final Map<String, AbstractLog> map = new HashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, AbstractLog> beansOfType = applicationContext.getBeansOfType(AbstractLog.class);
beansOfType.forEach((k, v) -> map.put(v.getName(), v));
}
public static AbstractLog getLog(String name) {
return map.get(name);
}
}
测试:
package atreus.ink.log;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class LogFactoryTest {
@Test
void getLogTest() {
AbstractLog javaLog = LogFactory.getLog("JavaLog");
javaLog.doLog();
System.out.println("----------");
AbstractLog cppLog = LogFactory.getLog("CppLog");
cppLog.doLog();
Assertions.assertTrue(true);
}
}
atreus.ink.log.AbstractLog#doLog
atreus.ink.log.JavaLog#doLog
----------
atreus.ink.log.AbstractLog#doLog
atreus.ink.log.CppLog#doLog
七、事务失效
Spring 事务失效场景:
- 数据库引擎不支持事务,比如 MySQL 的 MyISAM 引擎就不支持事务。
- 使用事务的类没有交由 Spring 管理,需要对事务类加
@Repository
、@Service
、@Controller
或@Component
注解。 - 方法不是
public
的,@Transactional
只能用于public
的方法上,如果要用在非public
方法上,可以开启AspectJ
代理模式。 - 自调用问题,由于 Spring 事务管理基于动态代理,而代理对象无法拦截类的内部调用,如果确实需要自调用,可以使用通过
AopContext.currentProxy()
手动获取代理对象或者在内部注入自己对应的 bean。 - 异常在函数内部被处理,只有将异常抛出 Spring 才能检测到异常并进行回滚。
八、三级缓存与循环依赖
Spring 三级缓存(以 singleton
作用域为例):
- 一级缓存:
singletonObjects
,缓存已经完成实例化和初始化的成品 bean。 - 二级缓存:
earlySingletonObjects
,缓存已经被其他对象引用的半成品 bean,这些 bean 被提前暴露。 - 三级缓存:
singletonFactories
,缓存未被其他对象引用的半成品 bean 的工厂,使用时通过这些工厂创建 bean。
三级缓存解决循环依赖(假设 A 和 B 之间存在循环依赖):
- 实例化 A,此时 A 还未完成属性赋值和初始化,A 只是一个半成品。
- 为 A 创建一个 bean 工厂,并放入到
singletonFactories
中。 - 发现 A 需要注入 B 对象,但是一、二、三级缓存均未发现对象 B。实例化 B,此时 B 还未完成属性赋值和初始化,B 只是一个半成品。
- 为 B 创建一个 bean 工厂,并放入到
singletonFactories
中。 - 发现 B 需要注入 A 对象,此时在三级缓存中发现了对象 A,从三级缓存中通过 bean 工厂得到对象 A,并将对象 A 移入二级缓存。
- 将对象 A 注入到对象 B 中。
- 对象 B 完成属性填充,执行初始化方法,移入一级缓存,此时对象 B 已经是一个成品。
- 对象 A 得到对象 B,将对象 B 注入到对象 A 中。
- 对象 A 完成属性填充,执行初始化方法,移入一级缓存,此时对象 A 也已经是一个成品。
两级缓存也能解决循环依赖,为什么还需要第三级缓存,第三级缓存又为什么缓存 bean 工厂而不是 bean呢?
其实第三级缓存的主要目的是延迟代理对象的创建,如果没有循环依赖的话,第三级缓存可以将代理对象的创建延迟到初始化完成之后,不需要提前。
具体来说,如果创建的 bean 是有代理的,那么注入的就应该是代理 bean,而不是原始的 bean。按照 Spring 的设计原则,Spring 会在完成属性赋值与依赖注入并且执行完初始化方法之后再为其创建代理。
对于三级缓存来说,如果需要创建代理,在没有循环依赖的情况下,Spring 首先会为已经实例化的 bean 在三级缓存中创建一个工厂,然后进行属性赋值、依赖注入和初始化,最后创建代理放入一级缓存,也即在完成初始化等操作后创建代理类。如果出现了循环依赖,Spring 会通过三级缓存中工厂类的方法去提前创建代理对象,放入二级缓存,在完成初始化等操作后再放入一级缓存,这种情况下就是先创建代理再初始化。