IOC
IOC 是什么,什么是 Spring IOC 容器?
IOC 是一种设计思想。 **IOC 容器是 Spring 用来实现 IOC 的载体, IOC 容器在某种程度上就是个Map(key,value),key是 name 属性,value 是对应的对象。**容器创建 Bean 对象, 使用依赖注入来管理对象之间的相互依赖关系,配置它们并管理它们的完整生命周期,很大程度上简化应用的开发,降低了耦合度。
容器通过读取提供的配置,比如 XML,注解或 Java 代码来接收对象信息进行实例化,配置和组装。
Spring IOC(Spring Inversion of Control)容器是 Spring 框架中的一个核心模块,用于管理和控制应用程序中的对象(也称为 bean)之间的依赖关系。Spring IOC 容器负责创建、配置和管理这些 bean,从而实现了IOC的原则。
Spring IOC 容器通过以下两种方式实现:
- Bean的定义文件(XML配置):您可以通过XML配置文件来定义应用程序中的 bean,包括它们的类型、属性和依赖关系。Spring IOC 容器会根据这些配置文件来实例化和组装 bean。
- 基于注解的配置:除了XML配置,Spring 也支持使用注解来标识和配置 bean。通过在类上使用注解,您可以告诉 Spring 容器如何创建和管理 bean
IOC 的实现机制/初始化流程
主要实现原理就是工厂模式加反射机制。
调用 refresh() 方法:
- 刷新准备,设置开始时间,状态, 初始化占位符等操作
- 获取内部的 BeanFactory,Spring 容器在启动的时候,先会保存所有注册进来的 Bean 的定义信息, 注册到 BeanFactory 中。
- 设置 BeanFactory 的类加载器和后置处理器,添加几个 BeanPostProcessor,手动注册默认的环境 bean
- 为子类提供后置处理 BeanFactory 的扩展能力,初始化上下文之前,可以复写 postProcessBeanFactory这个方法
- 执行 Context 中注册的 BeanFactory 后置处理器,对 SpringBoot 来说,这一步会进行 BeanDefintion 的解析
- 按优先级在 BeanFactory 注册 Bean 的后置处理器,这是在 Bean 初始化前后执行的方法
- 初始化国际化,事件广播器的模块,注册事件监听器
- 然后 Spring容器就会创建这些非延迟加载的单例 Bean
- 最后广播事件,ApplicationContext 初始化/刷新完成
什么是 Spring Bean
在Spring框架中,"Bean"是一个重要的概念,它表示由Spring IoC容器管理的对象。Bean可以是应用程序中的任何对象,包括服务、数据访问对象、控制器、实体等等。Spring IoC容器负责创建、配置和管理这些Bean的生命周期。
Spring Bean 的作用域有哪些?
- singleton(单例):
- 默认作用域,容器中只存在一个Bean实例。
- 当容器初始化时,会创建该Bean的实例,并在整个应用程序生命周期内共享。
- 所有对该Bean的请求都返回相同的实例引用。
- 适用于无状态的Bean,例如服务、工具类,以减少资源消耗和提高性能。
- prototype(原型):
- 每次请求都创建一个新的Bean实例。
- 容器不会缓存原型Bean的实例,每次都会创建新的Bean。
- 适用于有状态的Bean或需要频繁重建的Bean,每次请求都获取一个独立的实例。
- request(请求):
- 每个HTTP请求都创建一个新的Bean实例。
- 在Web应用中,每个HTTP请求都会创建一个新的Bean实例,该Bean仅在该请求的处理过程中存在。
- 适用于Web应用中需要请求级别状态的Bean。
- session(会话):
- 每个HTTP会话(session)都创建一个新的Bean实例。
- 在Web应用中,每个用户会话都会创建一个新的Bean实例,该Bean在整个会话期间存在。
- 适用于Web应用中需要会话级别状态的Bean。
- application(应用程序):
- 整个Web应用中共享一个Bean实例。
- 在Web应用中,该Bean在整个应用程序生命周期内存在,所有用户共享同一个实例。
- 适用于全局资源的共享,例如共享的配置信息。
- websocket(WebSocket):
- 每个WebSocket会话都创建一个新的Bean实例。
- 用于WebSocket应用中,每个WebSocket会话都会创建一个新的Bean实例,该Bean在WebSocket会话期间存在。
Spring Bean 的生命周期
Spring Bean的生命周期包括以下阶段:
-
实例化:当Spring容器启动时,它会根据配置文件或注解等信息,实例化(创建)Bean对象。Spring提供了多种方式来创建Bean,最常见的方式是使用默认构造函数实例化Bean。
-
设置Bean属性:一旦Bean对象被实例化,Spring容器会将Bean的属性设置为配置文件中定义的值,或者通过自动注入等方式将依赖注入到Bean中。
-
Bean的初始化:在Bean的所有属性都被设置之后,Spring容器会调用Bean的初始化方法(如果有的话)。初始化方法可以通过两种方式来定义:
- 使用@Bean注解的
initMethod
属性 - 实现InitializingBean接口,覆盖
afterPropertiesSet()
方法
这些方法可以用来执行一些初始化任务,例如建立数据库连接、加载配置文件等。
- 使用@Bean注解的
-
Bean可用:一旦Bean被初始化,它就可以被应用程序的其他部分使用了。
-
使用Bean:在Bean被初始化后,应用程序可以使用它执行相应的业务逻辑。
-
销毁Bean:当应用程序不再需要Bean时,Spring容器可以销毁它。销毁Bean时,容器会调用Bean的销毁方法(如果有的话)。销毁方法可以通过两种方式来定义:
- 使用@Bean注解的
destroyMethod
属性 - 实现DisposableBean接口,覆盖
destroy()
方法
这些方法可以用来执行一些清理任务,例如关闭数据库连接、释放资源等。
- 使用@Bean注解的
-
Bean被销毁:一旦Bean被销毁,它将不再可用,并且将从Spring容器中移除。
Spring 自动装配 bean 有哪些方式?
- 自动装配 by Type(按类型自动装配):Spring容器会自动根据属性的数据类型(Type)来匹配依赖的Bean。如果容器中有且仅有一个匹配的Bean,就会将其注入到属性中,如果有多个匹配,Spring会抛出异常,表示不确定性。
@Autowired
private SomeBean someBean;
- 自动装配 by Name(按名称自动装配):Spring容器会根据属性的名称来匹配依赖的Bean。属性的名称要与要注入的Bean的名称一致。
@Autowired
private SomeBean someBean;
- 自动装配 by Qualifier(按限定符自动装配):当有多个同类型的Bean存在时,可以使用
@Qualifier
注解指定要装配的Bean的名称,以消除歧义。
@Autowired
@Qualifier("specificBean")
private SomeBean someBean;
- 自动装配 by Constructor(通过构造函数自动装配):可以使用构造函数进行自动装配,Spring容器会根据构造函数的参数类型和Bean的类型来匹配依赖。
@Autowired
public SomeService(SomeBean someBean) {
this.someBean = someBean;
}
- 自动装配 by Primary(主要Bean自动装配):使用
@Primary
注解来标记一个Bean作为首选Bean,当多个同类型的Bean存在时,Spring会优先选择@Primary
注解标记的Bean进行装配。
@Component
@Primary
public class PrimaryBean implements SomeBean {
// ...
}
- 自动装配 by No(禁用自动装配):可以使用
@Autowired
的required
属性将自动装配禁用,如果找不到匹配的Bean,则不会抛出异常。
@Autowired(required = false)
private SomeBean someBean;
ApplicationContext和BeanFactory有什么区别?
ApplicationContext 和 BeanFactory 是两个核心的Spring容器,它们用于管理和创建Bean,但在功能和用途上有一些重要的区别:
初始化时间:
BeanFactory
是Spring容器的最基本形式,它在初始化时延迟创建Bean,即只有当应用程序首次请求一个Bean时才会创建它。ApplicationContext
是BeanFactory
的子接口,通常在容器启动时就会预加载所有的Bean,提前创建和初始化Bean,因此在第一次请求Bean时,不需要等待Bean的初始化过程。
配置资源加载:
BeanFactory
:它可以从各种外部资源(如XML文件、属性文件等)加载Bean的配置信息。通常需要通过编程方式配置BeanFactory,手动加载配置。ApplicationContext
:它是一个更高级的容器,可以从多种资源加载配置信息,包括XML文件、Java注解、Java配置类等。它提供了更多的功能,如国际化支持、事件发布、AOP等,通常用于大型应用程序。
Bean生命周期:
BeanFactory
:它仅负责创建和管理Bean,不提供高级功能,如事件处理、AOP等。如果只需要基本的Bean管理功能,可以使用BeanFactory
。ApplicationContext
:它扩展了BeanFactory
的功能,提供了更多的企业级功能,如国际化、事件发布、资源加载等。通常更适合中大型企业应用程序。
性能:
BeanFactory
:由于延迟初始化,通常在启动时占用的内存较少,但在第一次请求大量Bean时可能会有一定的性能开销,因为需要动态创建和初始化Bean。ApplicationContext
:由于预加载所有Bean,它在应用程序启动时可能会占用更多的内存,但在后续请求Bean时性能更高,因为Bean已经初始化完毕。
应用场景:
BeanFactory
:适用于资源受限的环境或需要更精细控制Bean创建和初始化的场景。ApplicationContext
:适用于大型企业级应用程序,需要更多高级功能和自动配置的场景。
什么是循环依赖?怎么解决循环依赖?
Spring Bean的循环依赖是指两个或多个Spring Bean之间相互依赖,形成一个循环引用的情况。这种循环依赖可能发生在Spring容器中,其中一个Bean需要另一个Bean的引用,而同时另一个Bean也需要第一个Bean的引用,形成一个闭环。这种情况可能会导致应用程序无法正常启动,或者在运行时产生不一致的行为。
循环依赖示例:
考虑以下两个简单的Spring Bean:
@Component
public class A {
private B b;
@Autowired
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
@Autowired
public B(A a) {
this.a = a;
}
}
在这个示例中,Bean A 依赖于Bean B,而Bean B 也依赖于Bean A,形成了一个循环依赖。
Spring框架通过使用三级缓存和"先创建实例,后填充属性"的方式来解决循环依赖问题。以下是Spring如何解决循环依赖的详细过程:
- Bean的注册与创建:
- 当Spring容器启动时,它会扫描并注册所有的Bean定义。
- 对于每个Bean,容器会在三级缓存中创建一个占位符,并尝试创建Bean的实例。
- 创建Bean实例(提前曝光):
- 在实例化Bean的过程中,如果发现循环依赖,Spring会创建一个尚未完全初始化的Bean实例,并将其存储在一级缓存(singletonObjects)中。这个实例是一个未填充属性的半成品。
- Spring创建这个半成品实例的目的是为了提前曝光,以满足其他Bean对该Bean的依赖。
- 填充属性:
- 继续创建Bean的实例,Spring会填充Bean的属性,包括依赖其他Bean的属性。
- 如果发现需要的依赖Bean已经在一级缓存中(之前创建的半成品实例),则将该依赖Bean注入到正在创建的Bean中。
- 循环依赖的处理:
- 当Bean A 需要依赖Bean B 时,Spring容器会首先创建半成品的Bean A,并将其放入一级缓存。
- 接着,Spring会创建半成品的Bean B,并发现Bean B 需要依赖 Bean A。此时,Spring会从一级缓存中获取Bean A(半成品)并注入到Bean B 中。
- 最后,Spring会继续初始化完整的Bean A 和 Bean B,分别填充其余的属性。
- Bean实例替换:
- 一旦Bean A 和 Bean B 都被完全初始化,它们将分别替换之前在一级缓存中的半成品实例。这意味着一级缓存中现在包含了完整的Bean实例,而不再是半成品。
- Bean的完整初始化意味着所有初始化方法(如
@PostConstruct
注解的方法)都已调用,属性都已填充。
通过上述机制,Spring能够解决循环依赖问题,同时确保所有Bean都能够在需要时被正确引用。需要注意的是,虽然Spring能够处理大多数情况的循环依赖,但过度复杂的依赖关系仍然可能导致问题,因此在设计应用程序时,应尽量避免复杂的循环依赖关系,以确保应用程序的可维护性和稳定性。
Spring中@Autowired和@Resource的区别?
@Autowired
和 @Resource
都是用于依赖注入的注解,主要有如下几个区别:
- 来源:
@Autowired
是 Spring 框架自带的注解,它基于类型(byType)进行依赖注入。@Resource
是 Java EE(J2EE)规范提供的注解,通常与 Spring 集成时也可以使用,它基于名称(byName)进行依赖注入。
- 注入规则:
@Autowired
按照变量的数据类型(类型匹配)来查找和注入依赖。@Resource
按照变量的名称(名称匹配)来查找和注入依赖。
- 默认情况:
@Autowired
默认按照类型查找依赖,如果找到多个匹配的依赖,会抛出异常;可以与@Qualifier
注解一起使用,指定要注入的 Bean 的名称。@Resource
默认按照名称查找依赖,可以指定name
属性来指定要注入的 Bean 的名称。
- 兼容性:
@Autowired
是 Spring 的特有注解,在 Spring 环境中使用。@Resource
是 Java EE 规范的一部分,可以在其他 Java EE 容器中使用,例如应用于 EJB 或其他 Java EE 技术的组件。
- 可选性:
@Autowired
注解的依赖通常是必须的,如果找不到匹配的 Bean,会引发异常。@Resource
注解的依赖通常是可选的,如果找不到匹配的 Bean,它将尝试使用默认名称(变量名)进行查找,如果还找不到,将允许该字段为 null。
AOP
什么是 AOP?
AOP(面向切面编程)是一种软件开发方法,它允许开发者在不改变主要业务逻辑的情况下,将横切关注点(cross-cutting concerns)与主要业务逻辑进行分离。横切关注点通常包括日志记录、性能监视、安全性、事务管理等。AOP 的主要目标是提高代码的模块化性、可维护性和可重用性。
AOP 的核心原理是什么?
Spring框架中AOP的核心原理是基于代理机制和面向切面编程来实现横切关注点的分离和模块化,以下是Spring框架中AOP的核心原理:
- 代理模式: Spring使用代理模式来实现AOP。在Spring AOP中,目标对象(包含主要业务逻辑)被包装在一个代理对象中。这个代理对象负责管理切面的引入和通知的执行。
- 切面(Aspect): 在Spring AOP中,切面是一个普通的Java类,它包含了通知和切点的定义。通知是横切关注点的具体操作,切点定义了通知应该在何处执行。切面通常使用注解或XML配置来定义。
- 连接点(Join Point): 连接点是在应用程序执行过程中可以插入切面的特定点,通常是方法调用、异常处理或字段访问等。Spring AOP支持方法级别的连接点。
- 通知(Advice): 通知是切面中的具体操作,它定义了在连接点何时、何地以及如何执行横切关注点的代码。Spring AOP支持不同类型的通知,包括前置通知、后置通知、环绕通知、异常通知和最终通知。
- 切点表达式(Pointcut Expression): Spring AOP使用切点表达式来定义切点,这是一种规则或表达式,它指定了在哪些方法上应该应用通知。切点表达式可以基于方法的名称、包名、类名等来指定切点。
- 织入(Weaving): 织入是将切面与目标对象关联的过程。Spring AOP支持多种织入方式,包括编译时织入、类加载时织入和运行时织入。最常见的是运行时织入,其中代理对象在运行时动态创建,将切面引入到目标对象的方法调用中。
为什么要引入 AOP?
Java OOP 存在一些局限性:
- 静态化语言:类结构一旦定义,不容易被修改
- 侵入性扩展:通过继承或组合组织新的类结构
通过 AOP 我们可以把一些非业务逻辑的代码(比如安全检查、监控等代码)从业务中抽取出来,以非入侵的方式与原方法进行协同。这样可以使得原方法更专注于业务逻辑,代码接口会更加清晰,便于维护。
简述 AOP 中几个比较重要的概念
- 切面(Aspect): 切面是AOP的核心概念之一。它是一个模块化单元,用于封装横切关注点的定义。切面包含了通知和切点的定义。通知定义了在连接点何时、何地以及如何执行横切关注点的代码。切点定义了在何处应该应用通知。切面通常包括多个通知和切点的组合,用于实现不同的横切关注点。
- 通知(Advice): 通知是切面中的具体操作,它定义了在连接点何时、何地以及如何执行横切关注点的代码。根据执行时机,通知可以分为以下几种类型:
- 前置通知(Before Advice): 在连接点之前执行,通常用于执行预处理操作,如参数验证。
- 后置通知(After Advice): 在连接点之后执行,通常用于执行清理操作,如资源释放。
- 返回通知(After Returning Advice): 在连接点成功返回后执行,通常用于记录方法返回值或执行后续处理。
- 异常通知(After Throwing Advice): 在连接点抛出异常后执行,通常用于处理异常情况。
- 环绕通知(Around Advice): 在连接点之前和之后执行,可以完全控制连接点的执行,包括是否执行连接点以及如何修改连接点的执行。
- 切点(Pointcut): 切点是切面中定义的条件,它指定了在哪些连接点应该应用通知。切点使用切点表达式来定义,该表达式通常基于方法的名称、包名、类名等来选择连接点。切点确定了通知的作用范围。
- 连接点(Join Point): 连接点是在应用程序执行过程中可以插入切面的特定点,通常是方法调用、异常处理或字段访问等。连接点是切点的具体实例。通知会在连接点处执行。
- 织入(Weaving): 织入是将切面与目标对象关联的过程。它是AOP框架在运行时或编译时将切面引入到应用程序中的方式。织入可以在不同的时机进行,如编译时、加载时或运行时。运行时织入是最常见的,其中代理对象在运行时动态创建,并将切面应用于目标对象的方法调用。
- 引入(Introduction): 引入是AOP的一个概念,允许切面向现有类添加新的方法或字段,而无需修改目标类的代码。引入允许切面在不改变类继承关系的情况下添加额外的功能。
- 目标对象(Target Object): 目标对象是包含主要业务逻辑的类,它将被切面影响。通常,切面在目标对象的方法调用之前或之后执行通知。
什么是 AOP 代理?
AOP(Aspect-Oriented Programming)代理是在AOP中使用的一种机制,它是实现横切关注点引入的关键组成部分。AOP代理是一个代理对象,它包含了目标对象(主要业务逻辑)和与之关联的切面(Aspect)。AOP代理在客户端代码与目标对象之间充当了中间人的角色,用于在方法调用前后执行横切关注点的操作。
两种常见的AOP代理类型是:
- JDK动态代理: 基于Java反射机制,使用Java标准库中的
java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口来创建代理对象。JDK动态代理只能代理接口,要求目标对象实现接口。 - CGLIB代理: CGLIB(Code Generation Library)代理通过创建目标对象的子类来实现代理。因为它是基于字节码操作的,所以可以代理非接口类型的目标对象。但需要注意的是,CGLIB代理要求目标对象没有被标记为
final
,否则无法生成代理。
请详细说明JDK 动态代理
JDK(Java Development Kit)动态代理是Java标准库提供的一种代理机制,用于创建代理对象,这些代理对象实现了一个或多个接口,并且可以在运行时动态生成。它通常用于代理实现了特定接口的类,实现横切关注点的引入。
- 代理对象的创建: JDK动态代理通过
java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口来创建代理对象。代理对象实现了目标接口,所以客户端可以将代理对象当作目标接口的实例来使用。 - InvocationHandler接口:
java.lang.reflect.InvocationHandler
是一个函数式接口,通常需要自定义一个类来实现该接口。在这个实现类中,你可以编写代理对象在方法调用前后执行的逻辑。它的核心方法是invoke
,接收三个参数:代理对象、方法对象和方法参数,然后返回方法执行的结果。 - 代理对象的创建方式: 使用
Proxy
类的newProxyInstance
方法来创建代理对象。这个方法需要传递三个参数:类加载器、目标接口数组和InvocationHandler
实例。例如:
MyInvocationHandler handler = new MyInvocationHandler();
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class[] { MyInterface.class },
handler
);
这里,proxy
就是代理对象,它实现了MyInterface
接口,handler
是自定义的InvocationHandler
实例,用于控制代理对象的行为。
- 代理方法的执行: 当客户端调用代理对象的方法时,实际上会触发
InvocationHandler
的invoke
方法。在invoke
方法中,你可以编写代码来处理横切关注点的逻辑,例如记录日志、执行额外操作等。然后,invoke
方法会调用目标对象的对应方法,并返回结果。
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在方法调用前执行横切关注点的逻辑
System.out.println("Before method execution");
// 调用目标对象的方法
Object result = method.invoke(target, args);
// 在方法调用后执行横切关注点的逻辑
System.out.println("After method execution");
// 返回方法执行的结果
return result;
}
}
限制条件: JDK动态代理要求目标对象必须实现至少一个接口。此外,它无法代理被标记为final
的类和方法,因为final
修饰的类和方法无法被继承和重写。
请详细说明 CGLIB 动态代理
CGLIB 动态代理则是基于类代理(字节码提升),通过 ASM(Java 字节码的操作和分析框架)将被代理类的 class 文件加载进来,修改其字节码生成一个子类。
- 代理对象的创建: CGLIB动态代理通过生成目标类的子类来创建代理对象。这个子类继承自目标类,并且可以覆盖目标类中的方法以添加代理逻辑。代理对象是目标类的子类的一个实例。
- 代理类的创建方式: CGLIB代理通常需要使用第三方库,例如
cglib-nodep
。你需要编写一个代理类,这个代理类继承自net.sf.cglib.proxy.MethodInterceptor
,然后在其中编写代理逻辑。例如:
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.Enhancer;
public class MyCglibProxy implements MethodInterceptor {
public Object createProxy(Class<?> targetClass) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetClass);
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object proxy, java.lang.reflect.Method method, Object[] args, net.sf.cglib.proxy.MethodProxy methodProxy) throws Throwable {
// 在方法调用前执行横切关注点的逻辑
System.out.println("Before method execution");
// 调用目标对象的方法
Object result = methodProxy.invokeSuper(proxy, args);
// 在方法调用后执行横切关注点的逻辑
System.out.println("After method execution");
// 返回方法执行的结果
return result;
}
}
- 代理对象的创建和使用: 使用CGLIB代理时,需要先创建一个
Enhancer
对象,然后设置目标类和回调对象(即上面的MyCglibProxy
),最后调用create
方法创建代理对象。代理对象可以像普通对象一样使用,但会触发回调对象的intercept
方法来执行横切关注点的逻辑。
MyCglibProxy cglibProxy = new MyCglibProxy();
MyService proxy = (MyService) cglibProxy.createProxy(MyService.class);
proxy.doSomething();
- 代理方法的执行: 当客户端调用代理对象的方法时,CGLIB代理会通过代理类的方法调用触发
intercept
方法的执行。在intercept
方法中,你可以编写代码来处理横切关注点的逻辑,然后调用methodProxy.invokeSuper
来调用目标对象的方法。
JDK 动态代理和 CGLIB 有什么区别?
代理对象类型:
- JDK动态代理: JDK动态代理是基于接口的代理,它要求目标对象必须实现一个或多个接口。代理对象是通过实现目标接口来创建的。
- CGLIB代理: CGLIB代理可以代理非接口类型的目标对象,它通过继承目标类并生成子类来创建代理对象。这使得CGLIB代理可以代理那些没有实现接口的类。
实现方式:
- JDK动态代理: JDK动态代理是通过Java标准库中的
java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口来实现的。代理对象实际上是一个实现了目标接口的匿名内部类。 - CGLIB代理: CGLIB代理是通过字节码操作技术来生成目标类的子类,并在子类中添加方法拦截逻辑。这种方式不需要目标类实现接口,但需要在运行时生成字节码,因此通常比JDK动态代理略慢。
性能:
- JDK动态代理: 由于JDK动态代理基于接口,通常在处理接口代理时性能较好。但对于类代理,性能可能稍逊于CGLIB代理,因为需要进行接口和反射相关的操作。
- CGLIB代理: CGLIB代理通常在性能上略优于JDK动态代理,特别是对于类代理,因为它不需要进行接口和反射相关的操作。
代理对象创建方式:
- JDK动态代理: JDK动态代理是通过
Proxy.newProxyInstance
方法来创建代理对象,需要传入目标对象的类加载器、接口数组和InvocationHandler
实例。 - CGLIB代理: CGLIB代理是通过CGLIB库来创建代理对象,它不需要目标对象实现接口,只需提供目标类即可。
限制条件:
- JDK动态代理: 由于基于接口,JDK动态代理无法代理那些没有实现接口的类。另外,它也无法拦截目标类内部自调用的方法。
- CGLIB代理: CGLIB代理可以代理没有实现接口的类,但无法代理被标记为
final
的类和方法。
Spring AOP 和 AspectJ 有什么关联?
Spring AOP: Spring AOP 是 Spring 框架内置的 AOP 框架,它提供了一种基于代理的轻量级 AOP 解决方案。Spring AOP 主要关注于对 Spring 容器管理的 bean 进行切面编程。它基于代理模式来实现 AOP,通常使用 JDK 动态代理或 CGLIB 来创建代理对象。Spring AOP 提供了对前置通知、后置通知、环绕通知等通知类型的支持,以及通过切点表达式来选择连接点。Spring AOP的主要优势在于它的集成性,可以与 Spring 的依赖注入和事务管理等功能无缝集成。
AspectJ: AspectJ 是一个独立的 AOP 框架,它提供了更强大和灵活的 AOP 功能。AspectJ 使用自定义的编译器或者在运行时织入字节码的方式来实现 AOP。相比于 Spring AOP,AspectJ 更为强大,可以应用于任何 Java 类,不仅限于 Spring 管理的 bean。它支持更多的通知类型和更复杂的切点表达式。AspectJ 还提供了一种静态织入的方式,可以在编译时或类加载时织入切面,而不仅仅是在运行时。
Spring AOP 整合 AspectJ 注解与 Spring IoC 容器,比 AspectJ 的使用更加简单,也支持 API 和 XML 的方式进行使用。不过 Spring AOP 仅支持方法级别的 Pointcut 拦截。
Spring AOP 中有哪些 Advice 类型?
前置通知(Before Advice): 前置通知在目标方法调用之前执行。它通常用于执行预处理操作,例如参数验证、日志记录、权限检查等。前置通知可以通过@Before
注解或XML配置来定义。
后置通知(After Returning Advice): 后置通知在目标方法正常返回后执行。它通常用于执行清理操作或记录返回值等。后置通知可以通过@AfterReturning
注解或XML配置来定义。
环绕通知(Around Advice): 环绕通知是最灵活的一种Advice类型,它可以在目标方法调用前后自定义处理逻辑。它可以完全控制方法的执行,包括是否执行目标方法以及如何修改方法的参数和返回值。环绕通知通常用于实现缓存、性能监控等复杂的横切关注点。环绕通知可以通过@Around
注解或XML配置来定义。
异常通知(After Throwing Advice): 异常通知在目标方法抛出异常后执行。它通常用于处理异常情况、进行错误日志记录或异常恢复操作。异常通知可以通过@AfterThrowing
注解或XML配置来定义。
最终通知(After (finally) Advice): 最终通知无论目标方法是否成功执行,都会执行。它通常用于执行清理操作,无论目标方法是否抛出异常。最终通知可以通过@After
注解或XML配置来定义。
Spring 事务
Spring 管理事务的方式有几种?
- 编程式事务管理: 这是一种通过编写代码来管理事务的方式。您可以在方法内部使用Spring的
PlatformTransactionManager
接口以及TransactionDefinition
来手动开始、提交或回滚事务。这种方式提供了最大的灵活性,但通常需要更多的代码。 - 基于XML配置的声明式事务管理: 这是一种通过XML配置来声明事务规则的方式。您可以在Spring的配置文件中定义事务管理器(例如
DataSourceTransactionManager
)以及事务通知(例如<tx:advice>
元素),然后将它们应用到特定的方法或类上。这种方式提供了更好的可维护性和可配置性,而不需要修改代码。 - 基于注解的声明式事务管理: 使用Spring的注解,例如
@Transactional
,可以在方法级别或类级别上声明事务规则。这种方式与XML配置方式类似,但更加方便,并将事务声明与业务逻辑代码分离。
事务注解@Transactional实现机制
当您在一个方法上添加@Transactional
注解时,Spring会在运行时动态生成一个代理对象,该代理对象包装了目标对象(包含@Transactional
注解的方法所在的类)。这个代理对象的主要任务是截获方法的调用,以便在方法执行前后添加事务管理逻辑。
在生成代理对象时,Spring会创建一个事务切面(Transaction Aspect
)。事务切面是一个横切关注点,它包含了事务管理的核心逻辑,如事务的开启、提交、回滚等。这个切面通过AOP的通知(Advice)机制来实现。
当客户端代码调用带有@Transactional
注解的方法时,代理对象首先检查当前线程是否已经存在一个事务上下文(Transaction Context
)。如果不存在,代理对象会使用配置的事务管理器来开启一个新的事务。然后,代理对象执行目标方法,目标方法内的数据库操作都会在这个新的事务中进行。
在目标方法执行完成后,代理对象会根据方法的执行情况来决定是提交事务还是回滚事务。如果方法正常完成,事务会被提交,否则,如果方法抛出了异常,事务会被回滚。这确保了事务的一致性和完整性。
@Transactional
注解还允许配置各种事务属性,例如传播行为、隔离级别、超时等,以满足不同场景下的事务需求。这些属性可以在注解中指定,Spring会根据配置来管理事务。
Spring 事务的传播行为
在Spring中,事务的传播行为是指在一个方法已经运行在一个事务上下文中时,另一个方法被调用时如何处理事务。Spring中常见的事务传播行为有如下几种:
- REQUIRED(默认):
- 如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务。
- REQUIRED 是最常见的传播行为,也是 Spring 默认的传播行为。
- SUPPORTS:
- 如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务方式执行。
- 如果要求方法始终在事务内执行,但允许它在没有事务的情况下执行,可以使用 SUPPORTS 传播行为。
- MANDATORY:
- 要求方法在一个事务内执行,如果当前没有事务,则抛出异常。
- 如果方法需要在已存在的事务内执行,MANDATORY 传播行为可以确保这一点。
- REQUIRES_NEW:
- 无论当前是否存在事务,都会创建一个新的事务。如果当前存在事务,它将被挂起。
- REQUIRES_NEW 传播行为允许方法始终在自己的事务内执行,与外部事务无关。
- NOT_SUPPORTED:
- 方法不应在事务内执行。如果当前存在事务,它将被挂起,方法将以非事务方式执行。
- NEVER:
- 方法绝对不能在事务内执行。如果当前存在事务,将抛出异常。
- NESTED:
- 如果当前存在事务,则创建一个嵌套事务,嵌套事务是外部事务的一部分。如果当前没有事务,则行为类似于 REQUIRED。
- NESTED 传播行为允许在嵌套事务内执行,如果嵌套事务失败,则只回滚嵌套事务,而不会影响外部事务。