Spring 篇
Spring框架中的单例bean是线程安全的吗?
不是线程安全的
Spring bean并没有可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。
Spring框架中有一个@Scope注解,默认的值就是singleton,单例的。因为一般在spring的bean的中都是注入无状态的对象,没有线程安全问题,如果在bean中定义了可修改的成员变量,是要考虑线程安全问题的,可以使用多例或者加锁来解决
当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性) ,则必须考虑线程同步问题。
如何处理 Spring 单例有状态的Bean 的线程安全问题?
- 将 Bean 的作用域由 “singleton” 单例 改为 “prototype” 多例。
- 在 Bean 对象中避免定义可变的成员变量,当然,这样做不太现实,就当我没说。
- 在类中定义 ThreadLocal 的成员变量,并将需要的可变成员变量保存在ThreadLocal 中,ThreadLocal 本身就具备线程隔离的特性,这就相当于为每个线程提供了一个独立的变量副本,每个线程只需要操作自己的线程副本变量,从而解决线程安全问题
Spring事务失效
非public方法
默认情况下,只有public修饰的方法才能被Spring的事务管理器拦截,非public方法会被忽略。
@Transactional是基于动态代理实现的,当创建bean实例时,Spring框架会扫描带有@Transactional注解的方法,并通过AOP技术生成代理对象并对事务进行管理。动态代理只能代理public方法。
异常被捕获并处理了
如果在@Transactional注解标记的方法中,出现了异常但是该异常被捕获并处理了,并没有抛出异常,那么这个事务就会正常提交,而不是回滚。
抛出检查异常
Spring 默认只会回滚非检查异常(不指定rollbackFor参数的前提下)
-
非检查异常:在编译时不需要强制处理的异常。这些异常一般是由程序逻辑错误或者系统内部错误导致的
- 例如空指针异常(NullPointerException)、数组越界异常(ArrayIndexOutOfBoundsException)等
-
检查异常:在编译时需要显式处理的异常。通常表示程序运行中可能出现的外部错误或不正常情况,通常表示程序运行中可能出现的外部错误或不正常情况
- 常见的检查异常包括IOException、SQLException等。
解决办法:
配置rollbackFor属性:@Transactional(rollbackFor=Exception.class)
没有开启事务注解支持
在Spring Boot项目中,需要在配置类上添加 @EnableTransactionManagement
注解来启用事务注解支持。
类内部调用@Transactional标注的方法
@Transactional的实现原理在于以该注解为切面坐标,在扫描动态生成的代理类中以此坐标来在其前后分别设置事务开启和事务提交操作的增强,那么这些的前提是在于,你的操作环境是在于Spring动态代理生成的代理类中,而不是this对应的本类
当一个Bean需要被注入其它Bean时,Spring会创建一个代理对象来代替真正的Bean对象。这个代理对象会拦截目标Bean对象的方法调用,并在方法调用前后进行一些操作,比如实现事务管理、缓存处理等。这样,通过代理对象的调用,就可以实现一些横切关注点的统一处理。
解决方法
Bean的生命周期
Spring 生命周期全过程大致分为五个阶段:
-
创建前准备阶段
- Bean 在开始加载之前,需要从上下文和相关配置中解析并查找 Bean 有关的扩展实现
-
创建实例阶段
- 这个阶段主要是通过反射来创建 Bean 的实例对象,并且扫描和解析 Bean 声明的一些属性
-
依赖注入阶段
-
被实例化的 Bean 存在依赖其他 Bean 对象的情况,则需要对这些依赖 bean 进行对象注入
同时,在这个阶段会触发一些扩展的调用,比如常见的扩展类:BeanPostProcessors(用来实现 bean 初始化前后的扩展回调)、InitializingBean(这个类有一个 afterPropertiesSet(),这个在工作中也比较常见)、BeanFactoryAware 等等。
@PostConstruct注解用于标记一个方法,该方法在类实例化后被调用,在依赖注入完成之后执行。它的作用是在对象创建后执行一些初始化操作——>[解决静态方法中调用注入Bean对象]
-
-
容器缓存阶段
- 容器缓存阶段主要是把 bean 保存到容器以及 Spring 的缓存中,到了这个阶段,Bean就可以被开发者使用了。
- BeanPostProcessors 方法中的后置处理器方法如:postProcessAfterInitialization,也会在这个阶段触发。
-
销毁实例阶段
- 当 Spring 应用上下文关闭时,该上下文中的所有 bean 都会被销毁。如果存在 Bean 实现了 DisposableBean 接口,或者配置了
destory-method
属性,会在这个阶段被调用。
- 当 Spring 应用上下文关闭时,该上下文中的所有 bean 都会被销毁。如果存在 Bean 实现了 DisposableBean 接口,或者配置了
- 通过BeanDefinition获取bean的定义信息
- 调用构造函数实例化bean
- bean的依赖注入
- 处理Aware接口(BeanNameAware、BeanFactoryAware、ApplicationContextAware)
- Bean的后置处理器BeanPostProcessor-前置
- 初始化方法(InitializingBean、init-method)
- Bean的后置处理器BeanPostProcessor-后置
- 销毁bean
Spring中的Bean都是代理Bean?
并非所有的Bean都会被声明为需要代理,只有被特定的AOP切面所切入的Bean才会生成AOP代理对象。其他非代理的Bean仍然会作为原始对象被实例化和注入。因此,虽然Spring加载的对象确实大部分是通过代理方式生成的AOP代理对象,但仍然存在一部分对象是原始对象。
例子说明
假设我们有两个Bean,分别是UserService和OrderService。其中,UserService需要被AOP代理,以实现事务管理的功能,而OrderService不需要被代理。
在Spring的配置中,我们可以通过对UserService添加@Transactional注解来标明该Bean需要生成AOP代理对象。而对于OrderService,则不需要做任何特殊的标记,它会作为原始对象被实例化和注入。
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Transactional
public void addUser(User user) {
userDao.addUser(user);
}
// other methods...
}
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
public void addOrder(Order order) {
orderDao.addOrder(order);
}
// other methods...
}
在上述代码中,UserService被标记为@Service注解,并且其中的addUser方法被标记为@Transactional注解,表示该方法需要被事务管理。因此,Spring会为UserService生成一个AOP代理对象,并在Bean初始化时将该代理对象注入到其他需要UserService依赖的Bean中。
而OrderService则没有任何特殊的注解标记,因此Spring会将其作为原始对象进行实例化和注入。
Bean循环依赖 / 三级缓存
三种形态
- 互相依赖:A 依赖 B,B 又依赖 A,它们之间形成了循环依赖。
- 三者间依赖:A 依赖 B,B 依赖 C,C 又依赖 A,形成了循环依赖。
- 是自我依赖:A 依赖 A 形成了循环依赖。
三级缓存
-
一级缓存: 缓存完全实例化且属性赋值完成的 Bean ,可以直接使用
- 限制bean在beanFactory中只存一份,即实现singleton scope,单靠它解决不了循环依赖
-
二级缓存: 缓存早期的bean对象(生命周期还没走完),只实例加载了,没有依赖注入,懒加载机制
-
三级缓存: 缓存的是ObjectFactory,存储的是代理的Bean的工厂对象,而不是代理的Bean本身。
- 三级缓存value存的不是bean,是ObjectFactory,用来生成早期Bean对象,并放入二级缓存中
执行流程
当我们调用getBean( )方法的时候,先去一级缓存找目标Bean,找不到就去二级缓存找,如果二级缓存也没有,意味着这Bean还没有被实例化,Spring就会去实例化该目标Bean,将其丢到二级缓存中,然后对该Bean标记是否具有循环依赖,如果没有循环依赖,则不动它,如果具有循环依赖,则等待下一次轮询进行再赋值,也就是解析@Autowired 注解。等@Autowired 注解赋值完成后,会将目标 Bean 存入到一级缓存。
可以说Spring的Bean是一种懒加载的机制。在Spring容器初始化时,并不会立即对所有的Bean进行属性的赋值,而是在需要使用Bean时才会进行属性的填充和赋值。
当调用getBean()方法获取Bean实例时,如果Bean已经在一级缓存中,则直接返回已经完成属性填充的Bean对象;如果Bean在二级缓存中,则检查是否存在循环依赖,如果不存在循环依赖,则创建一个代理对象返回,等待后续的属性填充;如果Bean还未实例化,则会进行实例化并放入二级缓存中,等待后续的属性填充和解析@Autowired注解的过程。
具备一二级,为什么需要第三级的缓存
循环依赖的可能是
AOP
代理之后的对象,而不是原始对象!如果不存在代理对象,二级缓存就可以解决循环依赖性的问题,但是当存在代理对象的时候,二级缓存则无法完全解决循环依赖,需要引入三级缓存
当使用 CGLIB 代理时,代理对象无法被缓存。这就意味着在二级缓存中,我们无法获取到代理对象。(如果不存在代理对象的时候可以借助早期Bean来实现提前曝光,但是如果该循环的对象被AOP这些修饰了,它被注入到其他类中的实例一定得是一个代理类实例)
如果需要被代理的类出现了循环依赖的情况,Spring必须在创建Bean时就创建代理对象,才能保证循环依赖的解决。这就违背了Spring的设计原则。因为在循环依赖的情况下,如果Bean之间的依赖没有完成,Bean就无法完成创建。如果此时再去为它们创建代理,可能会出现问题。
因此,为了解决循环依赖的问题,使用第三级缓存是比较合适的做法,如果发现循环对象需要被代理的,就将该对象通过JDK代理 / Cglib代理调用FactoryBean的getObject( )创建一个代理工厂对象,并将其该类的 BeanDefinition 信息一起存储到三级缓存中,在循环依赖并需要使用代理对象的情况下,就可以从三级缓存中获取所有必要的信息来创建代理对象来进行一个类似于曝光的操作。让该需要被代理的类所依赖的类创建出来,丢到一级缓存中,这个半成品代理对象再从一级缓存中拿到其依赖的类变得完整,最后将这代理对象放到一级缓存中。删除二级缓存的半成品代理对象
虽然它一样是基于早期的不完整的 Bean 实例来创建的,但是Spring出手维护了下,所以这个代理对象是可以被拿来当作曝光用的
具备一三级,为什么需要第二级的缓存
要知道,三级缓存中存储的都是代理对象的工厂对象,每次通过这个工厂对象的getObject方法获取到的代理对象都是不一样的噢,也就是不是单例的啦。
不能解决循环依赖的情况
-
多例 Bean 通过 setter 注入的情况,不能解决循环依赖问题
- 这是因为多例下你通过曝光得到了一个对象让自己完整了,但是那个对象想来引用你,哦吼,找不到你在哪了
- 解决办法: 加上延迟加载注解@Lazy,其效果是什么时候需要该对象才进行Bean对象创建
-
构造器注入的 Bean 的情况,不能解决循环依赖问题
-
本质上是因为构造器注入需要将对象的实例化和依赖注入两个步骤绑定为一个原子步骤,这样子就不能通过曝光来实现破局
-
解决办法: 加上延迟加载注解@Lazy,其效果是什么时候需要该对象才进行Bean对象创建
-
Spring 事务传播行为
Spring 为了解决这个问题,定义了 7 种事务传播行为。
- REQUIRED:默认的 Spring 事物传播级别,如果当前存在事务,则加入这个事务,如果不存在事务,就新建一个事务。
- REQUIRE_NEW:不管是否存在事务,都会新开一个事务,新老事务相互独立。外部事务抛出异常回滚不会影响内部事务的正常提交。
- NESTED:如果当前存在事务,则嵌套在当前事务中执行。如果当前没有事务,则新建一个事务,类似于 REQUIRE_NEW。
- SUPPORTS:表示支持当前事务,如果当前不存在事务,以非事务的方式执行。
- NOT_SUPPORTED:表示以非事务的方式来运行,如果当前存在事务,则把当前事务挂起。
- MANDATORY:强制事务执行,若当前不存在事务,则抛出异常.
- NEVER:以非事务的方式执行,如果当前存在事务,则抛出异常。
主要从你和我双方有没有的角度去回答
Bean作用域
理论上来说,常规的生命周期只有两种:
- singleton, 也就是单例,意味着在整个 Spring 容器中只会存在一个 Bean 实例。
- prototype,翻译成原型,意味着每次从 IOC 容器去获取指定 Bean 的时候,都会返回一个新的实例对象。
但是在基于 Spring 框架下的 Web 应用里面,增加了一个会话纬度来控制 Bean 的生命周期,主要有三个选择
- request, 针对每一次 http 请求,都会创建一个新的 Bean
- session,以 sesssion 会话为纬度,同一个 session 共享同一个 Bean 实例,不同的 session 产生不同的 Bean 实例
- globalSession,针对全局 session 纬度,共享同一个 Bean 实例
在Spring 5.0前的默认方案是懒汉式的单例,在首次被调用时才进行实例化,并且只实例化一次,后续的调用都返回同一个实例。从 Spring 5.0 开始,Spring 的单例 bean 默认采用的是饿汉式的实现,即在容器启动时就实例化并初始化单例 bean,而不是延迟加载
/*
Spring5.0前的懒汉式实现,主要采用的是双重检查锁定的方案
*/
public class SingletonBean {
// 声明静态的 SingletonBean 实例
private static volatile SingletonBean instance;
// 私有化构造函数,防止外部实例化
private SingletonBean() {}
// 获取 SingletonBean 实例的静态方法
public static SingletonBean getInstance() {
// 双重检查锁定,确保只有实例不存在时才进行实例化
if (instance == null) {
synchronized (SingletonBean.class) {
if (instance == null) {
instance = new SingletonBean();
}
}
}
return instance;
}
// 随便一个方法来确认单例成功创建了
public void doSomething() {
System.out.println("SingletonBean.doSomething() method is called.");
}
}
/*
在 Spring 5.0 或更新版本中,可以通过 @Configuration 注解配合 @Bean 注解来实现饿汉式创建Bean
*/
@Configuration
public class SingletonBean {
// 声明静态的 SingletonBean 实例
private static SingletonBean instance = new SingletonBean();
// 私有化构造函数,防止外部实例化
private SingletonBean() {}
// 提供静态方法获取 SingletonBean 实例
public static SingletonBean getInstance() {
return instance;
}
// 随便一个方法来确认单例成功创建了
public void doSomething() {
System.out.println("SingletonBean.doSomething() method is called.");
}
@Bean
public SingletonBean createSingletonBean() {
return getInstance();
}
}
如何将Bean纳入Spring的管理
- 使用 xml 的方式来声明 Bean 的定义,Spring 容器在启动的时候会加载并解析这个 xml,把 bean 装载到 IOC 容器中。
- 使用@CompontScan 注解来扫描声明了@Controller、@Service、 @Repository、@Component 注解的类。
- 使用@Configuration 注解声明配置类,并使用@Bean 注解实现 Bean 的定义
- 使用@Import 注解,导入配置类或者普通的 Bean
- 使用 FactoryBean 工厂 bean,动态构建一个 Bean 实例,Spring Cloud OpenFeign 里面的动态代理实例就是使用 FactoryBean 来实现的。
BeanFactory和FactoryBean的区别
在Spring中最核心的就是Ioc容器,它保存了所有需要对外提供的Bean的实例。Spring对外暴露的ApplicationContext作为IoC容器最重要的接口,它也实现了 BeanFactory
接口。
BeanFactory
用于访问Spring bean容器的根接口。这是Spring bean容器的基本客户端视图。原来是获取Spring Bean的接口,也就是IoC容器。我们更常用的ApplicationContext就是一个BeanFactory。我们通过bean的名称或者类型都可以从BeanFactory来获取bean
FactoryBean
是一个特殊的Bean,这个Bean可以返回创建Bean的工厂。
当我们想要从BeanFactory中获取创建Bean的Factory时,可以在beanName前面加上 & 符号就可以获得Bean对应的Factory。
FactoryBean在IOC容器的基础上,给Bean的实现加上了一个简单工厂模式和装饰模式。Bean的代理对象就是FactoryBean实现的
-
功能用途
- BeanFactory提供了依赖注入和控制反转的功能,不提供复杂的Bean初始化逻辑;FactoryBean支持复杂的Bean初始化逻辑或延迟初始化(eg:数据库连接、Bean的AOP代理)
-
返回值
- BeanFactory返回的直接就是Bean对象本身。我们可以使用BeanFactory的getBean()方法,通过传入FactoryBean的名称来获取FactoryBean生成的Bean对象。当传入的名称前面添加了"&"符号时,getBean()方法将返回FactoryBean本身。当你通过getBean方法获取一个普通的Bean实例时,getBean方法内部会直接从IoC容器中获取Bean实例,而不是走FactoryBean的getObject方法获取
- FactoryBean返回的是一个代理Bean或者包装Bean
总结:
二者都是用来创建对象的
当使用BeanFactory的时候必须遵循完整的创建过程,这个过程是由Spring来管理控制的
而使用FactoryBean只需要调用getObject就可以返回具体的对象,整个对象的创建过程是由用户自己来控制的,更加灵活.
BeanFactory和ApplicationContext有什么区别?
BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。
-
功能上的区别
- BeanFactory是Spring里面最底层的接口,包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化,控制bean的生命周期,维护bean之间的依赖关系。
- ApplicationContext接口作为BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能,如继承MessageSource、支持国际化、统一的资源文件访问方式、同时加载多个配置文件等功能。
-
加载方式的区别。
- BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
- 而ApplicationContext是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单例Bean,那么在需要的时候,不需要等待创建bean,因为它们已经创建好了。
相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。
-
创建方式的区别。
- BeanFactory通常以编程的方式被创建
- ApplicationContext还能以声明的方式创建,如使用ContextLoader。
-
注册方式的区别。
- BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。
Spring IoC 的工作流程
IOC 的全称是 Inversion Of Control, 也就是控制反转,它的核心思想是把对象的管理权限交给容器。
应用程序如果需要使用到某个对象实例,直接从 IOC 容器中去获取就行,不需要在一个类中去实例化另一个类对象,这样设计的好处是降低了程序里面对象与对象之间的耦合性。
Spring IOC 的工作流程大致可以分为两个阶段。
IOC 容器的初始化
首先,一个IoC容器应创建一个工厂(DefaultListableBeanFactory),可以使我们读取的资源文件可以存放。
然后根据程序中定义的 XML 或者注解等 Bean 的声明方式,通过解析和加载后生成 BeanDefinition,然后把 BeanDefinition 注册到 IOC容器 (注意:这里不是直接就是将该Bean的实例注册加载到容器中,而是Bean的BeanDefinition ) ,最后把这个 BeanDefinition 保存到一个 Map 集合里面,从而完成了 IOC 的初始化
BeanDefinition 是 Spring 容器中用来描述和定义一个 Bean 的元数据对象。它包含了关于 Bean 的一些重要信息,如 Bean 的类名、作用域、依赖关系、初始化方法、销毁方法等等。BeanDefinition 本质上是一个配置对象,用于告诉 Spring 容器如何创建和管理这个 Bean。
总结:BeanDefinition 描述了一个 Bean 应该具有的属性和行为,而 Bean 实例则是这个描述的具体实现。
对于设置lazy-init 属性为false的Bean,在容器初始化后就会进行实例化存储到Bean容器中
(调用了getBean()方法)Bean 初始化及依赖注入
然后进入到第二个阶段,这个阶段会做两个事情
-
通过反射针对没有设置 lazy-init 属性 (默认为true,懒加载) 的单例 bean 进行初始化。执行实例化之前的一些准备(初始化啊、事件处理器、注册组件等);
在 Spring 5.0后,默认情况下,单例作用域的 Bean 是在容器启动时就会被实例化和初始化的。但是如果你将单例 Bean 的
lazy-init
属性设置为true
,那么该 Bean 将会在第一次被使用时才会被实例化和初始化。 -
实例化Bean
-
检查和解析 Bean 之间的依赖关系,完成 Bean 的依赖注入。
@Autowired 和@Resource的注入区别
@Autowired
@Autowired 是 Spring 提供的一个注解,默认是根据类型来实现Bean 的依赖注入
@Autowired 注解里面有一个 required 属性默认值是 true,表示强制要求 bean 实例的注入,在应用启动的时候,如果 IOC 容器里面不存在对应类型的 Bean,就会报错。当然,如果不希望自动注入,可以把这个属性设置成 false
解决存在多个注入类型的实例
如果在 Spring IOC 容器里面存在多个相同类型的 Bean 实例。由于@Autowired 注解是根据类型来注入 Bean 实例的,所以 Spring 启动的时候,会提示一个错误,大概意思原本只能注入一个单实例 Bean但是在 IOC 容器里面却发现有多个,导致注入失败。
解决方法如下:
-
@Primary
- 表示主要的 bean,当存在多个相同类型的 Bean 的时候,优先使用声明了@Primary 的 Bean
-
@Qualifier
- 它可以根据 Bean 的名字找到需要装配的目标 Bean
@Autowired查找Bean的时间复杂度为O(n):
- @Autowired注解是按照类型进行查找的,如果有多个同类型的Bean,那么还需要配合@Qualifier注解来指定Bean的名称。当使用@Autowired注解进行依赖注入时,Spring需要遍历beanDefinitionMap中的所有Bean,找出所有类型匹配的Bean,然后再根据@Qualifier指定的名称进行筛选。由于需要遍历所有的Bean,所以@Autowired查找Bean的时间复杂度是O(n)。
@Resource
@Resource 是 JDK 提供的注解,只是 Spring 在实现上提供了这个注解的功能支持。
@Resource 可以支持ByName 和 ByType 两种注入方式,具体采用哪个取决于属性 name
和 type
如果两个属性都没配置,就先根据定义的属性名字去匹配,如果没匹配成功,再根据类型匹配。两个都没匹配到,就报错
@Resource查找Bean的时间复杂度为O(1):
- @Resource注解是按照name属性进行查找的,如果没有指定name属性,那么默认是按照字段名进行查找。在Spring的DefaultListableBeanFactory类中,有一个beanDefinitionMap成员变量,这是一个HashMap,用于存储所有的Bean定义信息。当使用@Resource注解进行依赖注入时,Spring会直接根据Bean的名称在beanDefinitionMap中进行查找。由于HashMap的查找时间复杂度是O(1),所以@Resource查找Bean的时间复杂度也是O(1)。
二者对比
- @Resource性能比@Autowire好很多,尤其是在bean个数较多的场景下。简单的说,@Resource相当于O(1),@Autowire相当于O(n)
- @Autowired 可以作用在Setter、构造器方法@Resource 只可以使用在Setter方法上(只能是单个单数的setter方法)
- Spring 容器在启动时,会扫描应用程序中已经定义的所有 Bean 定义(或者根据一定的规则生成 Bean 定义)。然后,它会通过反射机制实例化每个 Bean,并将其加入到容器中进行管理。二者底层都是通过反射机制去容器中找寻该Bean实例来进行注入,如果被注解标记的类尚未被实例化为 Bean,Spring 容器会在DefaultListableBeanFactory根据其类定义BeanDefinition 创建并实例化该 Bean 对象,加入到Ioc容器中并返回该实例对象。
Spring依赖注入的方式
- 构造器注入
- setter注入
- 接口注入(变量注入)
构造器注入
@Component
public class ExampleService {
private Dependency dependency;
public ExampleService(Dependency dependency) {
this.dependency = dependency;
}
// ...
}
在创建ExampleService
时,就会自动将一个Dependency
实例注入到ExampleService
中
优点:
- 依赖注入中使用的依赖对象是可选的,意思是注入的依赖对象是可以为NULL的
- 允许在类构造完成后重新注入
缺点:
- 注入对象不能被final修饰,,
final
修饰的变量表示它的值一旦被初始化后就不能再被修改。而在变量注入的情况下,Spring 框架在运行时会动态地将依赖注入到变量中,这意味着变量的引用可能会在实例化后的任意时间点进行修改。
Setter方法注入
@Component
public class ExampleService {
private Dependency dependency;
public void setDependency(Dependency dependency) {
this.dependency = dependency;
}
// ...
}
优点:
- 显式注明必须强制注入,通过强制指明依赖注入来保证这个类的运行,防止发生NullPointerException;
- 注入的对象可以使用final修饰;
- 可以避免循环依赖问题,如果存在循环依赖的话,Spring在项目启动的时候就会报错
缺点:
- 当你有十几个甚至更多对象需要注入时,构造函数的代码臃肿,看起来不太舒服;
接口注入(变量注入)
这种方式要求目标Bean实现特定的接口,并通过接口方法来设置依赖项。
public interface Dependency {
void doSomething();
}
@Component
public class ExampleDependency implements Dependency {
@Override
public void doSomething() {
System.out.println("Doing something...");
}
}
@Component
public class ExampleService {
private Dependency dependency;
@Autowired
public ExampleService(Dependency dependency) {
this.dependency = dependency;
}
public void performAction() {
// 使用依赖接口的方法
dependency.doSomething();
}
}
ExampleDependency
实现了Dependency
接口,并被标记为@Component
,成为一个由Spring管理的Bean。在ExampleService
中,通过构造函数注入Dependency
接口的实例。当调用performAction()
方法时,会使用注入进来的Dependency
实例执行相应的操作
- 可能会导致循环依赖问题,并且启动的时候不会报错,只有在使用那个bean的时候才会报错
- 变量不能被final修饰
Bean 的加载机制
- 加载Bean的配置信息(1.XML模式 2. 注解扫描模式 3.@Configuration配置类的@Bean配置模式)
- Spring 解析这些声明好的配置内容,将这些配置内容都转化为 BeanDefinition 对象,BeanDefinition 中几乎保存了配置文件中声明的所有内容
- 将 BeanDefinition 存到一个叫做 beanDefinitionMap 中。以 beanName 作为 Key,以 BeanDefinition 对象作为 Value。
- Spring 容器,根据 beanName 找到对应的 BeanDefinition,再去选择具体的创建策略 (具体什么创建策略得看你声明的这个Bean的作用域设置的是什么?默认是采用单例模式的创建策略)
- 当你通过getBean方法获取一个普通的Bean实例时,getBean方法内部会直接从IoC容器中获取Bean实例,而不是走FactoryBean的getObject方法获取;如果获取的是一个FactoryBean的Bean,则会调用FactoryBean的getObject方法来获取到该代理 or 自定义加工后的Bean实例
Spring AOP 原理流程
创建代理对象阶段
在 Spring 中,创建 Bean 实例都是从 getBean()方法开始的,在实例创建之后,Spring 容器将根据 AOP 的配置去匹配目标类的类名,看目标类的类名是否满足切面规则。如果满足满足切面规则,就会调用 ProxyFactory
创建代理 Bean并缓存到 IoC 容器中。
根据目标对象的自动选择不同的代理策略 (JDK动态代理 / Cglib动态代理) ,如果目标类实现了接口,Spring会默认选择 JDK Proxy,如果目标类没有实现接口,Spring 会默认选择 Cglib Proxy,当然,我们也可以通过配置强制使用 Cglib Proxy
拦截目标对象阶段
当用户调用目标对象的某个方法时,将会被一个叫做AopProxy 的对象拦截,然后按顺序执行符合所有 AOP 拦截规则的拦截器链。
调用代理对象阶段
Spring AOP 拦截器链中的每个元素被命名为 MethodInterceptor,其实就是切面配置中的 Advice 通知。这个回调通知可以简单地理解为是新生成的代理 Bean 中的方法。也就是我们常说的被织入的代码片段,这些被织入的代码片段会在这个阶段执行
调用目标对象阶段
MethodInterceptor 接口也有一个 invoke()方法,在 MethodInterceptor 的 invoke()方法中会触发对目标对象方法的调用,也就是反射调用目标对象的方法。 (动态代理的本质)
Cglib 动态代理底层实现
JDK动态代理虽然简单易用,但是其有一个致命缺陷是,只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么Java动态代理就没法使用了。
Cglib的工作原理主要分为两个步骤:
- 生成代理类:Cglib通过读取被代理类的字节码,借助
ASM库
来修改字节码,生成一个新的类作为代理类。代理类是被代理类的子类,并且会重写被代理类中的非final方法,将代理逻辑插入到这些方法中。这些重写方法会在调用时首先执行代理逻辑,然后再调用原始的被代理类方法。 - 创建代理对象:一旦生成了代理类的字节码,就可以使用Java的反射机制实例化代理对象。Cglib通过创建代理对象的过程来连接代理类和被代理对象。在代理对象进行方法调用时,实际上是通过调用代理类中重写的方法,从而触发代理逻辑的执行。
CGLIB缺点:
- 对于final方法,无法进行代理。
- 由于Cglib是通过修改字节码来实现动态代理的,所以生成代理类的过程需要消耗一定的时间和资源。
Spring通知有哪些类型?
- 前置通知(Before):在目标方法被调用之前调用通知功能;
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知(After-returning ):在目标方法成功执行之后调用通知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的逻辑。
Spring 用到了哪些设计模式?
-
单例模式: 在Spring中定义的bean默认是单例模式。
-
工厂模式:Spring使用工厂模式通过BeanFactory、ApplicationContext创建Bean对象。
-
代理模式:Spring AOP功能的实现是通过代理模式中的动态代理实现的。
-
策略模式:Spring中资源访问接口Resource的设计是一种典型的策略模式。Resource接口是所有资源访问类所实现的接口,Resource 接口就代表资源访问策略,但具体采用哪种策略实现,Resource 接口并不理会。客户端程序只和 Resource 接口耦合,并不知道底层采用何种资源访问策略,这样客户端程序可以在不同的资源访问策略之间自由切换。
-
适配器模式:Spring AOP的增强或通知使用到了适配器模式。
环绕通知是最强大的一种通知类型,它同时包含了前置通知和后置通知的功能,并且可以完全控制方法的执行。
在实现环绕通知时,需要使用到适配器模式。具体来说,需要创建一个继承自 Spring 的 MethodInterceptor 接口的类,该类中实现 invoke 方法,该方法中包含了环绕通知的实现代码。
为了将 MethodInterceptor 接口的类与目标对象绑定起来,在 Spring AOP 中使用了适配器模式,具体来说,适配器模式将 MethodInterceptor 接口的方法调用转换为了目标对象的方法调用,从而实现了通知的功能。
-
装饰器模式:Spring 中配置 DataSource 的时候,DataSource 可能是不同的数据库和数据源,项目需要连接多个数据库,这种模式让我们可以根据客户需求切换不同的数据源。
-
模板模式:Spring中jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,就是用到了模板模式。