【面试必问】Spring核心之控制反转(IOC)
博主介绍: ✌博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家✌
Java知识图谱点击链接:体系化学习Java(Java面试专题)
💕💕 感兴趣的同学可以收藏关注下 ,不然下次找不到哟💕💕
1、如何理解 IOC
1.1、什么是 Spring Bean
Spring Bean是Spring框架中的一个核心概念,它是一个由Spring容器管理的对象。在Spring中,Bean是指任何一个由Spring容器所管理的对象,可以是Java类的实例、数据源、事务管理器等等。
Spring Bean的创建、初始化、销毁等生命周期都由Spring容器控制,这样可以将应用程序的对象解耦合,提高模块化和可重用性。Spring Bean可以通过注解、XML配置文件等方式进行声明和配置,Spring容器会根据配置信息创建对应的Bean对象并将其注入到需要使用的地方。
在使用Spring框架时,Bean是非常重要的概念,理解和掌握Bean的创建和管理方式对于开发高质量的Spring应用程序非常重要。
public class HelloWorld {
private String message;
public void setMessage(String message) {
this.message = message;
}
public void getMessage() {
System.out.println("Your Message : " + message);
}
}
然后,在Spring的配置文件中声明该Bean:
<bean id="helloWorld" class="com.example.HelloWorld">
<property name="message" value="Hello World!"/>
</bean>
上述配置文件中,id属性指定了Bean的唯一标识符,class属性指定了Bean的类型,property元素用于设置Bean的属性。
最后,在Java代码中使用该Bean
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
obj.getMessage();
}
以上就是一个Bean的创建和使用。
1.2、Spring Bean 是单例的吗?
默认情况下,Spring Bean 是单例的,也就是说,Spring 容器只会创建一个 Bean 实例,并在需要时返回该实例。这是因为单例模式可以提高系统性能,避免了频繁创建和销毁对象的开销。
如果需要创建多个实例,可以通过在配置文件中设置 scope 属性来实现。例如,将 scope 属性设置为“prototype”表示该 Bean 的实例是多例的,每次请求都会创建一个新的实例。
需要注意的是,如果一个 Bean 是单例的,那么在多线程环境下访问时可能会存在线程安全问题。因此,在编写 Bean 的代码时需要注意线程安全问题,或者通过设置 scope 属性为“prototype”来避免线程安全问题,如下:
@Service
@Scope("prototype")
public class UserService {
// ...
}
1.3、什么是 IOC
IOC(Inversion of Control,控制反转)是一种设计模式,它将对象之间的依赖关系的控制权从程序代码中转移到了容器中,通过容器来实现对象的创建、销毁、管理和依赖注入等操作,从而降低了代码的耦合度,提高了代码的可维护性和可扩展性。
在传统的编程模式中,对象之间的依赖关系是在程序代码中直接实现的,这样会导致代码的耦合度很高,难以维护和扩展。而采用IOC模式,将对象之间的依赖关系的控制权交给容器来管理,程序代码只需要定义好依赖关系,容器就可以自动地完成对象的创建、销毁、管理和依赖注入等操作,从而降低了代码的耦合度,提高了代码的可维护性和可扩展性。
在Java开发中,Spring框架就是一个典型的IOC容器,它通过XML配置文件或注解的方式来管理对象之间的依赖关系,实现了对象的创建、销毁、管理和依赖注入等操作。
1.4、IOC 的好处
IOC模式的主要作用是降低程序代码的耦合度,提高代码的可维护性和可扩展性。具体来说,IOC模式可以带来以下几个方面的好处:
- 降低代码的耦合度:将对象之间的依赖关系的控制权交给容器来管理,程序代码只需要定义好依赖关系,不需要关心对象的创建、销毁、管理和依赖注入等操作,从而降低了代码之间的耦合度。
- 提高代码的可维护性:采用IOC模式,可以使程序代码更加清晰、简洁,易于理解和维护。
- 提高代码的可扩展性:采用IOC模式,可以方便地添加、修改、替换对象之间的依赖关系,从而实现代码的可扩展性。
- 提高代码的测试性:采用IOC模式,可以方便地进行单元测试和集成测试,从而提高代码的测试性。
- 降低代码的重复性:采用IOC模式,可以避免在程序代码中重复创建对象,从而降低了代码的重复性。
总之,采用IOC模式可以使程序代码更加灵活、易于维护和扩展,是现代软件开发中的一种重要的设计模式。
1.4、什么是 DI
DI是Dependency Injection(依赖注入)的缩写,是一种设计模式,也是面向对象编程中的一个重要概念。DI的主要作用是降低程序代码的耦合度,提高代码的可维护性和可扩展性。
依赖注入是指在创建对象时,将对象所依赖的其他对象的引用作为参数传递给对象的构造函数或者其他方法中,从而实现对象之间的依赖关系。这样,对象之间的依赖关系就不再由程序代码直接控制,而是由容器来管理。这样可以使程序代码更加灵活、易于维护和扩展。
依赖注入有三种方式:构造函数注入、属性注入和方法注入。其中,构造函数注入是最常用的一种方式,也是最推荐的一种方式。在构造函数注入中,依赖关系是在对象创建时就确定的,这样可以保证对象的依赖关系在整个生命周期中都是稳定的,从而提高代码的可维护性和可测试性。
依赖注入是现代软件开发中的一个重要概念,采用依赖注入可以使程序代码更加灵活、易于维护和扩展,是面向对象编程中的一种重要的设计模式。
总而言之,IOC 是设计思想,而 DI 是设计实现
2、IOC 的三种配置方式
IOC(Inversion of Control,控制反转)是一种设计模式,通过将对象的创建和依赖关系的管理交给容器来实现,从而降低程序代码的耦合度,提高代码的可维护性和可扩展性。IOC 的三种配置方式包括 XML 配置、注解配置和 Java 配置,下面分别用代码说明:
1. XML 配置方式:
假设我们有一个 UserService 接口和一个 UserServiceImpl 实现类,其中 UserServiceImpl 依赖于 UserDao 接口和 RedisTemplate 对象。我们可以通过 XML 配置文件来管理它们的依赖关系,示例代码如下:
UserService 接口:
public interface UserService {
void save(User user);
}
UserServiceImpl 实现类:
public class UserServiceImpl implements UserService {
private UserDao userDao;
private RedisTemplate redisTemplate;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void save(User user) {
userDao.save(user);
redisTemplate.opsForValue().set(user.getId(), user);
}
}
XML 配置文件:
<bean id="userService" class="com.example.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<property name="redisTemplate" ref="redisTemplate"/>
</bean>
<bean id="userDao" class="com.example.UserDaoImpl"/>
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
</bean>
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="localhost"/>
<property name="port" value="6379"/>
</bean>
2. 注解配置方式:
我们可以通过注解来管理对象的依赖关系,示例代码如下:
UserServiceImpl 实现类:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private RedisTemplate redisTemplate;
@Override
public void save(User user) {
userDao.save(user);
redisTemplate.opsForValue().set(user.getId(), user);
}
}
UserDao 接口:
public interface UserDao {
void save(User user);
}
UserDaoImpl 实现类:
@Repository
public class UserDaoImpl implements UserDao {
@Override
public void save(User user) {
// save user
}
}
RedisTemplate 对象的配置和 XML 配置方式相同。
3. Java 配置方式:
我们可以通过 Java 配置类来管理对象的依赖关系,示例代码如下:
UserServiceImpl 实现类:
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
private RedisTemplate redisTemplate;
@Autowired
public UserServiceImpl(UserDao userDao, RedisTemplate redisTemplate) {
this.userDao = userDao;
this.redisTemplate = redisTemplate;
}
@Override
public void save(User user) {
userDao.save(user);
redisTemplate.opsForValue().set(user.getId(), user);
}
}
UserDao 接口和 UserDaoImpl 实现类同注解配置方式。
RedisTemplate 的配置通过 Java 配置类来实现:
@Configuration
public class AppConfig {
@Bean
public RedisTemplate redisTemplate() {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(jedisConnectionFactory());
return redisTemplate;
}
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
jedisConnectionFactory.setHostName("localhost");
jedisConnectionFactory.setPort(6379);
return jedisConnectionFactory;
}
}
3、依赖注入的三种方式
依赖注入(Dependency Injection,DI)是一种实现控制反转(Inversion of Control,IoC)的设计模式,它通过将对象的依赖关系由调用者转移到外部容器中,以实现松耦合和可维护性。依赖注入一般分为三种方式:构造函数注入、Setter方法注入和接口注入。
下面是三种方式的代码举例:
1. 构造函数注入
public class UserServiceImpl implements UserService {
private UserDao userDao;
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
//...
}
public class UserController {
private UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
//...
}
// 创建对象时进行依赖注入
UserDao userDao = new UserDaoImpl();
UserService userService = new UserServiceImpl(userDao);
UserController userController = new UserController(userService);
2. Setter方法注入
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
//...
}
public class UserController {
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
//...
}
// 创建对象后通过Setter方法进行依赖注入
UserDao userDao = new UserDaoImpl();
UserService userService = new UserServiceImpl();
userService.setUserDao(userDao);
UserController userController = new UserController();
userController.setUserService(userService);在这里插入代码片
3. 注解注入
public interface UserDao {
//...
}
public class UserDaoImpl implements UserDao {
//...
}
public interface UserService {
//...
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
//...
}
4、IOC 源码剖析
Spring框架就是基于IoC思想实现的一个轻量级的容器,它可以帮助开发者管理对象之间的依赖关系,从而提高代码的可维护性和可扩展性。
下面我们来剖析Spring框架中IoC的实现原理:
1. IoC容器
Spring框架中的IoC容器是一个核心组件,它负责管理应用中的所有Bean对象。在Spring中,IoC容器分为两种类型:BeanFactory和ApplicationContext。
BeanFactory是IoC容器的基础接口,它提供了Bean的基本管理功能,如Bean的创建、销毁等。ApplicationContext是BeanFactory的子接口,它在BeanFactory的基础上增加了更多的功能,如国际化、事件处理等。
2. Bean的定义和注册
在Spring中,Bean的定义通过BeanDefinition来表示。BeanDefinition中包含了Bean的类名、属性等信息,它是IoC容器管理Bean的基础。
Bean的注册是通过BeanFactory或ApplicationContext来完成的。在注册Bean时,需要指定Bean的名称和对应的BeanDefinition。
下面是一个简单的BeanDefinition的代码示例:
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(UserServiceImpl.class);
beanDefinition.getPropertyValues().add("userDao", new RuntimeBeanReference("userDao"));
在上面的代码中,我们创建了一个GenericBeanDefinition对象,并设置了Bean的类名和依赖关系。其中,RuntimeBeanReference表示对另一个Bean的引用。
3. Bean的实例化
在IoC容器启动时,会根据BeanDefinition创建对应的Bean实例。Bean的实例化是通过BeanFactory或ApplicationContext中的BeanFactoryPostProcessor和BeanPostProcessor来完成的。
BeanFactoryPostProcessor是在Bean实例化之前执行的,它可以修改BeanDefinition中的属性,或者添加新的BeanDefinition。BeanPostProcessor是在Bean实例化之后执行的,它可以对Bean进行后置处理,如初始化、销毁等。
下面是一个简单的BeanFactoryPostProcessor的代码示例:
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition("userService");
beanDefinition.getPropertyValues().add("userDao", new RuntimeBeanReference("userDao"));
}
}
在上面的代码中,我们创建了一个MyBeanFactoryPostProcessor对象,并实现了postProcessBeanFactory方法。该方法会在Bean实例化之前被调用,我们可以在该方法中修改BeanDefinition中的属性。
4. Bean的依赖注入
在IoC容器启动时,会自动将Bean之间的依赖关系进行注入。依赖注入是通过BeanFactory或ApplicationContext中的AutowiredAnnotationBeanPostProcessor来完成的。
AutowiredAnnotationBeanPostProcessor是一个BeanPostProcessor,它会扫描所有的Bean,并自动注入它们之间的依赖关系。在注入依赖关系时,它会根据BeanDefinition中的属性进行匹配,并自动创建依赖对象。
下面是一个简单的依赖注入的代码示例:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
//...
}
在上面的代码中,我们使用了@Autowired注解来标记依赖关系。在IoC容器启动时,会自动将UserDao对象注入到UserServiceImpl中。
这里只是大致讲了下,因为如果要展开细致将会非常长,有兴趣的可以看看我另一篇帖子,是一张图,比较细致的描绘了IOC 的创建Bean的过程,IOC详解
5、IOC 常见问题
5.1、为什么推荐构造器注入方式?
推荐使用构造器注入方式的原因主要有以下几点:
显式表达依赖关系:使用构造器注入方式可以明确地表达依赖关系,开发者可以清晰地知道哪些依赖是必须的,哪些是可选的。
可以保证依赖完整性:使用构造器注入方式可以保证依赖的完整性,即只有在所有必须的依赖都被注入后,才能创建对象。这样可以避免因为缺少依赖而导致的运行时异常。
更容易进行单元测试:使用构造器注入方式可以更容易进行单元测试,因为可以直接传入模拟的依赖对象,而不需要依赖于IoC容器。
可以避免循环依赖问题:使用构造器注入方式可以避免循环依赖问题,因为只有在所有必须的依赖都被注入后,才能创建对象,从而避免了循环依赖的情况。
使用构造器注入方式可以使依赖关系更加清晰明确,保证依赖完整性,更容易进行单元测试,并且避免循环依赖问题。
5.2、我在使用构造器注入方式时注入了太多的类导致Bad Smell怎么办?
如果在使用构造器注入方式时注入了太多的类,导致代码出现了Bad Smell,可以考虑使用依赖注入容器来管理依赖关系。依赖注入容器可以自动地创建对象并注入依赖,从而减少手动注入的工作量,同时也可以减少代码的耦合度,提高代码的可维护性和可测试性。
在使用依赖注入容器时,可以将需要注入的依赖关系配置在容器中,容器会自动创建对象并注入依赖。这样可以避免手动注入过多的类,从而减少代码的复杂度和维护成本。
同时,也可以考虑使用依赖倒置原则,将高层模块依赖于抽象接口,而不是具体实现。这样可以减少依赖关系的复杂度,提高代码的可扩展性和可维护性。
如果在使用构造器注入方式时注入了太多的类导致Bad Smell,可以考虑使用依赖注入容器来管理依赖关系,同时也可以使用依赖倒置原则来减少依赖关系的复杂度。
5.3、@Autowired和@Resource以及@Inject等注解注入有何区别?
@Autowired、@Resource和@Inject是三种常见的依赖注入注解,它们的作用都是将一个Bean注入到另一个Bean中。它们的区别如下:
1. @Autowired
@Autowired是Spring框架提供的注解,它可以自动装配一个Bean。它默认按照类型(class)进行匹配,如果找到多个匹配的Bean,则按照Bean的名称进行匹配。如果找不到匹配的Bean,则会抛出异常。@Autowired还支持通过required属性来控制是否必须注入,如果required为true,则必须注入成功,否则会抛出异常。
2. @Resource
@Resource是Java EE提供的注解,它也可以自动装配一个Bean。它默认按照名称进行匹配,如果找不到匹配的Bean,则会抛出异常。@Resource还支持通过name属性来指定Bean的名称,也支持通过type属性来指定Bean的类型。
3. @Inject
@Inject是JSR-330提供的注解,它也可以自动装配一个Bean。它默认按照类型(class)进行匹配,如果找到多个匹配的Bean,则会抛出异常。@Inject还支持通过@Named注解来指定Bean的名称,也支持通过@Qualifier注解来指定Bean的类型。
@Inject注解可以用来注入一个Bean到另一个Bean中。以下是一个使用@Inject注解的示例:
假设我们有一个UserService接口和一个UserServiceImpl实现类,我们需要在另一个类中使用UserService,可以通过@Inject注解来注入UserService实例,代码如下:
public class UserController {
@Inject
private UserService userService;
public void getUserList() {
List<User> userList = userService.getUserList();
// do something with userList
}
}
在上面的代码中,我们使用@Inject注解将UserService实例注入到UserController类中的userService属性中。这样,在getUserList()方法中就可以直接使用userService来调用UserService中的方法了。
需要注意的是,为了使用@Inject注解,需要在项目中引入javax.inject包,这个包通常是由Java EE容器提供的,如果使用Spring框架,可以通过在pom.xml文件中添加以下依赖来引入javax.inject包:
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
总的来说,@Autowired是Spring框架提供的注解,@Resource是Java EE提供的注解,@Inject是JSR-330提供的注解。它们的作用都是将一个Bean注入到另一个Bean中,区别在于匹配规则、支持的属性和异常处理方式等方面。在实际使用中,可以根据具体的需求和场景来选择使用哪种注解。
💕💕 本文由激流丶创作,原创不易,感谢支持!
💕💕喜欢的话记得点赞收藏啊!
- 点赞
- 收藏
- 关注作者
评论(0)