IOC(Inversion of Control,控制反转)是Spring框架的核心。它是一种设计思想,将传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。
相较于传统的程序设计中,我们通常是在某个对象中主动创建另一个对象,控制权在自己手上。而在IOC中,创建对象的控制权被反转了,转到了Spring框架,当我们需要某个对象时从容器池中进行注入即可。
举例说明:
假设我们有一个简单的应用,包含一个UserService和一个DatabaseConnection:
public class DatabaseConnection {
// 数据库连接逻辑
}
public class UserService {
private DatabaseConnection dbConnection;
public UserService() {
this.dbConnection = new DatabaseConnection(); // 主动创建依赖对象
}
}
在这个例子中,UserService主动创建了DatabaseConnection的实例。这种方式会导致UserService和DatabaseConnection之间的强耦合。
使用Spring IOC后,我们可以这样改写:
public class UserService {
private DatabaseConnection dbConnection;
public UserService(DatabaseConnection dbConnection) {
this.dbConnection = dbConnection; // 依赖由外部注入
}
}
// 在Spring配置文件中
<bean id="dbConnection" class="com.example.DatabaseConnection" />
<bean id="userService" class="com.example.UserService">
<constructor-arg ref="dbConnection" />
</bean>
在这个IOC的例子中,UserService不再主动创建DatabaseConnection,而是通过构造函数参数接收一个DatabaseConnection实例。Spring容器负责创建这些对象,并将它们装配在一起。
例如,如果我们需要更换数据库连接方式,只需要修改Spring配置,而不需要修改UserService的代码。
例如,在测试UserService时,我们可以轻松地注入一个模拟的DatabaseConnection:
@Test
public void testUserService() {
DatabaseConnection mockConnection = mock(DatabaseConnection.class);
UserService userService = new UserService(mockConnection);
// 进行测试...
}
例如,如果我们需要为所有的Service类添加日志功能,我们可以通过修改Spring配置来统一处理,而不需要修改每个Service类。
例如,我们可以轻松地为所有的Service方法添加事务支持:
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
tx:attributes>
tx:advice>
<aop:config>
<aop:pointcut id="serviceOperation" expression="execution(* com.example.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/>
aop:config>
DI(Dependency Injection,依赖注入)是实现IOC的一种方式。在DI中,对象的依赖关系由容器在运行期决定,即由容器动态地将依赖对象注入到组件之中。
Spring提供了多种依赖注入的方式,主要包括:
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
<bean id="userService" class="com.example.UserService">
<constructor-arg ref="userRepository" />
bean>
public class UserService {
private UserRepository userRepository;
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
<bean id="userService" class="com.example.UserService">
<property name="userRepository" ref="userRepository" />
bean>
public class UserService {
@Autowired
private UserRepository userRepository;
}
public class UserService {
private UserRepository userRepository;
@Autowired
public void init(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
每种注入方式都有其适用场景,选择哪种方式取决于具体的需求和设计考虑。通常,构造器注入是最推荐的方式,因为它可以保证依赖的完整性,并支持不可变对象的创建。
在Spring中,构成应用程序主干并由Spring IOC容器管理的对象称为Bean。Bean是一个被实例化、组装和管理的对象。
Bean的定义包含了配置元数据,这些配置元数据告诉容器如何创建Bean,它的生命周期详情,及它的依赖关系。
以下是定义一个Bean的简单例子:
<bean id="userService" class="com.example.UserService">
<property name="userRepository" ref="userRepository"/>
bean>
在这个例子中,我们定义了一个id为"userService"的Bean,它的类是com.example.UserService,并且它依赖于另一个id为"userRepository"的Bean。
使用注解方式,我们可以这样定义一个Bean:
@Component
public class UserService {
@Autowired
private UserRepository userRepository;
}
在这个例子中,@Component注解告诉Spring这个类应该被当作一个Bean来管理,@Autowired注解表示userRepository应该被自动注入。
Spring框架支持以下几种bean的作用域:
@Bean
@Scope("singleton")
public UserService userService() {
return new UserService();
}
这意味着无论你从容器中获取多少次这个Bean,都是同一个实例。
@Bean
@Scope("prototype")
public UserService userService() {
return new UserService();
}
这意味着每次你从容器中获取这个Bean,都会得到一个新的实例。
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserService userService() {
return new UserService();
}
这种作用域主要用于Web应用程序中,每个HTTP请求都会有自己的Bean实例。
@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserPreferences userPreferences() {
return new UserPreferences();
}
这种作用域允许你为每个用户会话创建一个Bean实例,例如存储用户的偏好设置。
@Bean
@Scope(value = "globalSession", proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserPreferences userPreferences() {
return new UserPreferences();
}
Portlet规范定义了全局Session的概念,它被所有构成某个portlet web应用的各种不同的portlet所共享。
BeanFactory是Spring IOC容器的核心接口,它负责管理所有的Bean,包括Bean的创建、配置和管理。BeanFactory使用懒加载策略,只有当客户端请求某个Bean时,才会创建该Bean的实例。
BeanFactory提供了以下主要功能:
以下是一个使用BeanFactory的简单例子:
public class Main {
public static void main(String[] args) {
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
UserService userService = (UserService) factory.getBean("userService");
userService.doSomething();
}
}
在这个例子中,我们创建了一个XmlBeanFactory(BeanFactory的一个实现),它从classpath中读取一个XML配置文件。然后我们使用这个factory来获取一个名为"userService"的Bean。
FactoryBean是一种特殊的Bean,它可以生产或修饰对象实例。当你需要对一个Bean进行复杂的初始化时,可以实现FactoryBean接口。Spring容器会识别该Bean为FactoryBean,并使用它的getObject()
方法返回最终想要使用的Bean。
FactoryBean接口定义如下:
public interface FactoryBean<T> {
T getObject() throws Exception;
Class<?> getObjectType();
boolean isSingleton();
}
以下是一个FactoryBean的例子,它用于创建一个复杂的数据源对象:
public class DataSourceFactoryBean implements FactoryBean<DataSource> {
private String driverClassName;
private String url;
private String username;
private String password;
@Override
public DataSource getObject() throws Exception {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
@Override
public Class<?> getObjectType() {
return DataSource.class;
}
@Override
public boolean isSingleton() {
return true;
}
// setters for properties
}
在Spring配置中,我们可以这样使用这个FactoryBean:
<bean id="dataSource" class="com.example.DataSourceFactoryBean">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="user"/>
<property name="password" value="password"/>
bean>
当其他Bean需要注入DataSource时,Spring会调用DataSourceFactoryBean的getObject()方法来获取实际的DataSource对象。
ObjectFactory是一个函数式接口,用于延迟查找或创建对象。它主要用于解决循环依赖问题,我们稍后会详细讨论这个问题。
ObjectFactory接口定义如下:
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}
ObjectFactory的主要用途是延迟对象的创建或获取。例如,在处理prototype作用域的Bean时,我们可能需要在每次使用时都获取一个新的实例,这时就可以使用ObjectFactory:
public class UserManager {
private ObjectFactory<User> userFactory;
public void setUserFactory(ObjectFactory<User> userFactory) {
this.userFactory = userFactory;
}
public void doSomethingWithNewUser() {
User user = userFactory.getObject(); // 每次调用都获取一个新的User实例
// 使用新的User实例进行操作
}
}
在这个例子中,每次调用doSomethingWithNewUser方法时,都会通过userFactory获取一个新的User实例。这种方式特别适用于需要频繁创建新实例的场景。
ApplicationContext是BeanFactory的子接口,它扩展了BeanFactory的功能。除了提供IOC容器的基本功能外,ApplicationContext还提供了以下的功能:
@Autowired
private MessageSource messageSource;
public void displayMessage() {
String message = messageSource.getMessage("greeting", null, Locale.US);
System.out.println(message);
}
@Component
public class EmailService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void sendEmail(String to, String content) {
// 发送邮件的逻辑
eventPublisher.publishEvent(new EmailSentEvent(this, to, content));
}
}
@Component
public class EmailMonitor {
@EventListener
public void onEmailSent(EmailSentEvent event) {
System.out.println("Email sent to: " + event.getTo());
}
}
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 或者
ApplicationContext context = new FileSystemXmlApplicationContext("C:/config/applicationContext.xml");
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(servletContext);
ApplicationContext在启动时就实例化所有单例Bean(即使加载),相比BeanFactory(懒加载),它的启动时间更长,但运行时的性能更好。
以下是使用ApplicationContext的一个简单例子:
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.doSomething();
}
}
在这个例子中,我们创建了一个ClassPathXmlApplicationContext,它会从类路径中加载applicationContext.xml文件。然后我们使用这个context来获取一个UserService的Bean实例。
循环依赖是指两个或多个Bean互相依赖对方,形成一个闭环。这种情况下,Spring IOC容器在尝试创建这些Bean时会陷入死循环。
例如,考虑以下场景:
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
在这个例子中,A依赖B,B又依赖A,形成了一个循环依赖。
为了便于后续理解,这里先简单说一下bean的生命周期:
Spring中bean的生命周期包括以下步骤:
BeanDefinition
获取bean的定义信息(每一个bean都会被封装成一个BeanDefinition)。@Autowired
注解。Aware
接口的bean。BeanPostProcessor
的前置处理器。InitializingBean
接口或自定义的init-method
。BeanPostProcessor
的后置处理器,可能在这里产生代理对象。总结
主要把握创建过程和销毁过程这两个大的方面; 创建过程:首先实例化Bean,并设置Bean的属性,根据其实现的Aware接口(主要是BeanFactoryAware接口,BeanFactoryAware,ApplicationContextAware)设置依赖信息, 接下来调用BeanPostProcess的postProcessBeforeInitialization方法,完成initial前的自定义逻辑;afterPropertiesSet方法做一些属性被设定后的自定义的事情;调用Bean自身定义的init方法,去做一些初始化相关的工作;然后再调用postProcessAfterInitialization去做一些bean初始化之后的自定义工作。这四个方法的调用有点类似AOP。 此时,Bean初始化完成,可以使用这个Bean了。 销毁过程:如果实现了DisposableBean的destroy方法,则调用它,如果实现了自定义的销毁方法,则调用之。
接着再来说解决循环依赖的问题:
spring对循环依赖的处理有三种情况:
①构造器的循环依赖:这种依赖spring是处理不了的,直 接抛出BeanCurrentlylnCreationException异常。
@Lazy
懒加载注解,延迟bean的创建直到实际需要时。②单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖。
③非单例循环依赖:无法处理。
下面分析单例模式下的setter使用三级缓存来解决循环依赖问题:
让我们通过一个例子来详细说明这个过程:
@Component
public class A {
@Autowired
private B b;
public A() {
System.out.println("Creating A");
}
}
@Component
public class B {
@Autowired
private A a;
public B() {
System.out.println("Creating B");
}
}
A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A。
A首先完成了初始化的第一步(实例化),并且将自己提前曝光到singletonFactories(三级缓存)中。
此时进行初始化的第二步,A发现自己依赖对象B,此时就尝试去获取B对象,但发现B还没有被创建,所以开始去创建B。
B在初始化的时候发现自己依赖了对象A,于是尝试获取A对象,先尝试从singletonObjects(一级缓存,此时一级缓存中没有A对象,因为A还没初始化完全),接着尝试从earlySingletonObjects(二级缓存,二级缓存也没有A),最后尝试从singletonFactories(三级缓存)获取,由于A通过三级缓存将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(尽管此时的A对象还未完全初始化),同时将A对象放入到二级缓存中去,B拿到A对象后顺利完成了初始化阶段,完全初始化之后将自己放入到一级缓存singletonObjects中。
此时再返回A的初始化过程中,A此时就能从一级缓存中拿到B的对象并顺利完成自己的初始化阶段,最终A也完成了初始化,进入了一级缓存singletonObjects中,至此A、B对象都能创建成功。
简而言之:
ObjectFactory
存入三级缓存。ObjectFactory
存入三级缓存。ObjectFactory
并不会被清除。三级缓存仍然可以用于动态创建对象或返回代理对象。后置处理器在对象初始化完成后执行,可以根据需要返回代理对象,而不是直接返回原始对象。三级缓存是存储对象工厂,用于创建对象或者代理对象。一级缓存中的对象通常是原始对象,而代理对象需要通过三级缓存的工厂进行重新创建。如果只有二级缓存,A类如果需要被代理,B类在注入A类时应该得到A的代理对象,直接使用二级缓存就只能得到A类对象而非A的代理对象。
为什么不直接在放入二级缓存时判断是否需要代理,如果需要就在放入二级缓存时放入一个代理对象:在正常的bean生命周期中,代理对象应该是在对象初始化后进入后置处理器去生成代理的(也就是前面bean生命周期的第7步),而此时这样操作就会违背他的生命周期(放入二级缓存阶段应该是处于填充属性注入依赖的阶段,也就是前面bean生命周期的第3步)
具体来说:
通过使用三级缓存,Spring可以确保在循环依赖的情况下,所有引用到的Bean都是最终一致的(可能是代理对象)。
让我们通过一个例子来说明为什么需要三级缓存:
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
@Aspect
@Component
public class AAspect {
@Around("execution(* com.example.A.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Before A method execution");
Object result = pjp.proceed();
System.out.println("After A method execution");
return result;
}
}
在这个例子中,A需要被代理(因为有切面),而B不需要。如果只有二级缓存,那么在解决循环依赖时,B中注入的A可能是原始对象而不是代理对象。
使用三级缓存,Spring可以在需要的时候(即在解决循环依赖时)创建代理对象(代理对象实则就是对一级缓存的原始对象再进行一个包装)。这样可以确保所有地方引用的都是同一个对象(代理对象)。
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在通过分离横切关注点来增加模块化。它允许你在不修改代码自身的情况下,动态地添加额外的行为到现有的代码中。
横切关注点是指那些影响多个类的功能,如日志记录、事务管理、安全检查等。这些功能往往散布在应用程序的多个部分,导致代码重复和难以维护。AOP提供了一种方法来集中处理这些横切关注点。
AOP的主要概念包括:
目标对象(Target):被增强的对象。
代理对象(Proxy):对目标对象进行增强后的对象
连接点(Join Point):目标对象中可以被增强的方法。
切点(Pointcut):匹配连接点的表达式,也就是实际被增强的方法。
通知(Advice):在切面的某个特定连接点上执行的动作,即增强部分的代码逻辑。主要类型包括:
织入(Weaving):通知和切入点动态组合的过程
切面(Aspect):通知和切点的组合。
下面是一个简单的AOP示例,使用Spring AOP来实现日志功能:
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().getName());
}
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("After method: " + joinPoint.getSignature().getName());
System.out.println("Result: " + result);
}
}
在这个例子中,我们定义了一个切面(LoggingAspect),它包含两个通知:
通过使用AOP,我们可以在不修改原有业务逻辑的情况下,轻松地为多个类和方法添加日志功能。这大大提高了代码的模块化程度和可维护性。
Spring AOP默认使用JDK动态代理。但是,如果目标类没有实现接口,Spring会自动切换到使用CGLIB代理。
关于更多的介绍及区别可参考:Java代理详解:静态代理、动态代理-CSDN博客
在Spring中,如果你想强制使用CGLIB代理,可以通过以下配置:
@EnableAspectJAutoProxy(proxyTargetClass = true)
或者在XML配置中:
<aop:aspectj-autoproxy proxy-target-class="true"/>
特性 | Spring AOP | AspectJ |
---|---|---|
实现方式 | 基于代理(JDK动态代理或CGLIB) | 编译时或加载时织入 |
织入时机 | 运行时(运行时代理) | 编译时、类加载时或运行时 |
支持的切点 | 方法级切点 | 方法级、构造函数级、字段级等多种切点 |
性能 | 相对较低(由于代理的开销) | 较高(直接在字节码中插入切面逻辑) |
复杂性 | 简单易用,适合大多数应用场景 | 复杂,适合需要高级功能的场景 |
配置方式 | 基于XML或注解 | 需要使用AspectJ特定的语法和工具 |
支持的功能 | 事务管理、日志记录、权限控制等 | 更强大的功能,如字段拦截、构造函数拦截 |
集成 | 与Spring框架紧密集成 | 可以与Spring集成,但需要额外配置 |
学习曲线 | 较低,易于上手 | 较高,需要学习AspectJ的语法和概念 |