1.什么是IoC
Spring IoC(Inversion of Control,控制反转)是Spring框架的核心之一,它是一种设计模式,也称为依赖注入(Dependency Injection,DI)。在传统的程序设计中,对象之间的依赖关系通常由程序员在类内部直接创建和管理,而在IoC容器中,对象的创建和管理由容器来负责,程序员只需要定义对象的依赖关系,由容器来实现对象的创建和组装,即把对象的控制权交给IoC容器,所以叫控制反转。
接下来我们通过一个例子来类比解释什么是IoC
1.1 传统程序的开发
需求:造一辆汽车。
实现思路:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘大小设计车身,最后根据车身设计好整个汽车。在这个过程中出现了一个“依赖”关系,汽车依赖车身,车身依赖底盘,底盘依赖轮子。
最后实现的代码如下:
public class Main {public static void main(String[] args) {Car car = new Car();car.run();}
}class Car {private Framework framework;public Car() {framework = new Framework();System.out.println("car init");}public void run() {System.out.println("car run run!");}
}//车身
class Framework {private Bottom bottom;public Framework() {bottom = new Bottom();System.out.println("Framework init");}
}//底盘
class Bottom {private Tire tire;public Bottom() {tire = new Tire();System.out.println("bottom init");}
}//轮子
class Tire {//轮子尺寸private int size;public Tire() {this.size = 15;System.out.println("tire init");}
}
如果现在要求轮子的大小要为20,我们就要给Tire类添加一个有参数的构造方法,与此同时,Bottom类,Framework类,Car类都需要修改,如果,我现在又要给轮子添加一个属性 颜色 此时所有依赖Tire类的类又都要修改,由此可见这样的开发方式是存在弊端的。
1.2 IoC 程序开发
public class Main {public static void main(String[] args) {Tire tire = new Tire();Bottom bottom = new Bottom(tire);Framework framework = new Framework(bottom);Car car = new Car(framework);car.run();}
}class Car {public Car(Framework framework) {System.out.println("car init");}public void run() {System.out.println("car run run!");}
}//车身
class Framework {public Framework(Bottom bottom) {System.out.println("Framework init");}
}//底盘
class Bottom {public Bottom(Tire tire) {System.out.println("bottom init");}
}//轮子
class Tire {//轮子尺寸private int size;public Tire() {this.size = 15;System.out.println("tire init");}
}
我们把创建对象交给Main类去做,达到了其他类之间的解耦合效果,无论其他类怎么变化,整个的调用链是不需要调整的,这里我们的Main类就可以称作IoC容器。
在Spring中,IoC容器负责管理Java对象之间的依赖关系。当应用程序启动时,IoC容器会读取配置文件或注解信息,根据这些信息来实例化对象,并将这些对象之间的依赖关系注入到对象中。这种方式使得应用程序的组件之间解耦,易于维护和扩展。
1.3 DI
DI,全称为依赖注入(Dependency Injection),是面向对象编程中的一种设计模式。它是控制反转(Inversion of Control,IoC)思想的一种具体实现方式。
简单来说,依赖注入是指在创建对象时,将其所依赖的其他对象的引用注入到对象中,而不是由对象自己创建或查找依赖的对象。这样做的目的是降低模块之间的耦合度,使得对象之间的依赖关系更加灵活、可维护和易于测试。
2. IoC/DI 使用
我们已经对IoC和DI有初步的了解,接下来我们具体学习Spring IoC 和 DI的代码实现。
Spring 容器管理的主要是对象,这些对象,我们称之为“Bean” ,我们把这些对象交由Spring管理,由Spring来负责对象的创建和销毁,我们程序只需要告诉Spring,哪些需要存,以及如何从Spring取出对象。
使用@Component注解,可以表示把某个类交给Spring来管理,使用@Autowired注解注入运行时依赖的对象:
public class Main {public static void main(String[] args) {Car car = new Car();car.run();}
}
@Component
class Car {@Autowiredprivate Framework framework;public Car() {System.out.println("car init");}public void run() {System.out.println("car run run!");}
}//车身
@Component
class Framework {@Autowiredprivate Bottom bottom;public Framework() {System.out.println("Framework init");}
}//底盘
@Component
class Bottom {@AutowiredTire tire;public Bottom() {System.out.println("bottom init");}
}//轮子
@Component
class Tire {//轮子尺寸private int size;public Tire() {this.size = 15;System.out.println("tire init");}
}
3. IoC详解
3.1 Bean的存储
在上面的案例中,我们知道要把某个对象交给IoC容器管理,需要在类上添加一个注解:@Component 而Spring框架为了更好的服务Web程序,提供了更丰富的注解:
- 类注解:@Controller,@Service,@Repository,@Component,@Configuration
- 方法注解:@Bean
3.1.1 类注解
- @Controller
@Controller 注解用于标识一个类作为Spring MVC框架中的控制器,负责处理用户请求,并返回相应的视图。通常用于标识处理HTTP请求的控制器类,可以与 @RequestMapping 注解结合使用,实现请求映射和页面跳转。 - @Service
@Service 注解用于标识一个类作为业务层的组件,通常用于标识服务层的类,通过该注解告诉Spring该类是业务逻辑处理的组件,可以被注入到其他类中使用。 - @Repository
@Repository 注解用于标识一个类作为数据访问层的组件,通常用于标识数据访问对象(DAO)类,通过该注解告诉Spring该类是用于数据库访问的组件,可以捕获数据库异常并将其重新抛出为Spring的数据访问异常。 - @Component
@Component 注解是Spring中所有具体组件的通用形式,用于标识一个类作为Spring容器管理的组件,表示该类会自动被Spring加载,并可以通过依赖注入来使用。 - @Configuration
@Configuration 注解用于标识一个类作为配置类,通常用于定义Bean的创建和依赖关系。该注解通常与 @Bean 注解一起使用,用于替代XML配置文件,实现Java Config的方式进行配置。
这个和我们上期讲的应用分层是呼应的,让程序员看到类注解后,就能知道当前类的用途:
使用演示:
这些注解的使用方式都相同,我们通过@Controller来演示:
@Controller//将对象储存到Spring中
public class UserController {public void say() {System.out.println("Hello @Controller");}
}
如上述代码,我们只需在类上面加上@Controltroller 注解即可把该类的对象储存在Spring中。
我们如何知道这个对象已经存在Spring容器中了呢,接下来我们学习如何从Spring容器中获取对象:
@SpringBootApplication
public class J20240323SpringIocApplication {public static void main(String[] args) {//获取Spring上下文对象ApplicationContext context = SpringApplication.run(J20240323SpringIocApplication.class, args);//从Spring上下文中获取对象UserController userController = context.getBean(UserController.class);//调用say方法userController.say();}
}
可以看到,项目启动后成功打印了 Hello @Controller
解释:
ApplicationContext:译为程序上下文,用于表示 Spring 容器,并且负责管理 Bean 的生命周期和配置信息。即可以理解为包含了Spring容器里的所有内容,所有我们可以从ApplicationContext对象中获取Bean.
getBean()是BeanFactory接口中的一个方法,ApplicationContext实现了BeanFactory接口,我们打开BeanFactory的源码:
我们发现重载了五个getBean方法 ,我们刚才是通过类对象获取的Bean,所以我们使用的是第四个,我们接下来再学习第一个和第二个的使用:
1. getBean(String name)
根据Bean的名称获取Bean,Spring会给管理的Bean按照小驼峰的方式命名:
@SpringBootApplication
public class J20240323SpringIocApplication {public static void main(String[] args) {//获取Spring上下文对象ApplicationContext context = SpringApplication.run(J20240323SpringIocApplication.class, args);//从Spring上下文中获取对象//通过类对象获取UserController userController1 = context.getBean(UserController.class);userController1.say();//通过对象名获取UserController userController2 = (UserController)context.getBean("userController");userController2.say();}
}
注意:Spring官方文档中关于Bean的名称有这样一个注意事项:
当类名的前两个字符都是大写字母时,将按照类起初的名字来命名。
2. getBean(String name, Class<T> requiredType)
通过名称和类对象获取Bean:
@SpringBootApplication
public class J20240323SpringIocApplication {public static void main(String[] args) {//获取Spring上下文对象ApplicationContext context = SpringApplication.run(J20240323SpringIocApplication.class, args);//从Spring上下文中获取对象//通过类对象获取UserController userController1 = context.getBean(UserController.class);userController1.say();//通过对象名获取UserController userController2 = (UserController)context.getBean("userController");userController2.say();//通过类对象和对象名获取UserController userController3 = context.getBean("userController", UserController.class);userController3.say();}
}
注意:上述几种方式获取到的对象都是同一个对象,把类交给Spring管理时,默认是单例模式。
ApplicationContext对比BeanFactory:
上面我们演示的是使用ApplicationContext来获取Bean,我们也可以使用BeanFactory获取Bean,接下来我们讲解一下它们的区别和联系
区别:
- 初始化时机:BeanFactory 是在第一次请求获取 Bean 时才进行实例化和初始化,延迟加载;而 ApplicationContext 在容器启动时就将所有 Bean 进行实例化和初始化。
- 性能:ApplicationContext 比 BeanFactory 更强大也更复杂,因为 ApplicationContext 支持更多的特性,如国际化处理、事件传播、AOP 管理等,所以在性能上 BeanFactory 会更轻量级一些。
- 功能:ApplicationContext 继承自 BeanFactory,并且提供了更多的高级特性,如自动装配、消息资源处理、事件传播等。因此,ApplicationContext 功能更加强大。
联系:
- ApplicationContext 可以看作是 BeanFactory 的扩展版本,提供了更多功能和便利,是更高级的容器接口。
- 无论是 BeanFactory 还是 ApplicationContext,它们都负责管理 Bean 实例的生命周期、依赖注入等工作,是 Spring 框架中重要的组件。
- 在实际开发中,如果应用对性能要求较高,可以使用 BeanFactory;如果需要更多的企业级特性和功能,建议使用 ApplicationContext。通常情况下,我们更倾向于使用 ApplicationContext,因为它提供了更多的便利和功能,能够更好地支持复杂的应用场景。
3.1.2 方法注解
类注解是添加到类上面的,但是存在两个问题:
- 使用外部包里的类,没办法添加类注解
- 一个类需要多个对象
这种场景就需要使用方法注解@Bean
使用方法:
@Bean注解需要在类注解注解下的类中使用,使用时创建一个函数,返回一个想要交给Spring管理的对象即可:
@Controller
public class UserController {public void say() {System.out.println("Hello @Controller");}@Beanpublic User getUser1() {return new User();}@Beanpublic User getUser2() {return new User();}
}
注意:使用@Bean注解交给Spring管理的对象的名称和方法名一致,获取Bean时,如果对应类的对象在Spring中存在多个,则不能只通过类对象获取:
@SpringBootApplication
public class J20240323SpringIocApplication {public static void main(String[] args) {User user1 = (User) context.getBean("getUser1", User.class);user1.say();User user2 = (User) context.getBean("getUser2", User.class);user2.say();System.out.println(user1 == user2);}
}
3.2 扫描路径
一个对象要被Spring管理,不仅需要加上注解,还需要被Spring扫描到才行,Spring的默认扫描路径是启动类所在的目录及子目录,我们可以通过@ComponentScan注解修改扫描路径:
@ComponentScan(basePackages = "com.example")
@SpringBootApplication
public class J20240323SpringIocApplication {public static void main(String[] args) {SpringApplication.run(J20240323SpringIocApplication.class, args);}
}
4. DI 详解
4.1 属性注入
属性注入就是通过@Autowired注解,注入,前面我们简单的演示过,只需把@Autowired写在对应的引用上即可获取对应对像的引用
@Service
public class User {public void say() {System.out.println("helle");}
}
@Controller
public class UserController {@Autowiredprivate User user;public void say() {user.say();}
}
@SpringBootApplication
public class J20240323SpringIocApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(J20240323SpringIocApplication.class, args);UserController userController = context.getBean(UserController.class);userController.say();}
}
4.2 构造方法注入
我只需在需要引入依赖的类中添加一个初始化的构造方法,即可完成依赖注入
@Controller
public class UserController {private User user;public UserController(User user) {this.user = user;}public void say() {user.say();}
}
运行代码后同样能打印hello
注意:交给Spring管理的类,Spring会使用反射获取到对应类的构造方法来创建对象,如果使用构造方法注入,并且本类中存在多个构造函数,需要使用@Autowired指定使用哪个,否者可能会出错:
@Controller
public class UserController {private User user;public UserController() {}@Autowiredpublic UserController(User user) {this.user = user;}public void say() {user.say();}
}
4.3 Setter 方法注入
给需要注入依赖的类添加带@Autowired注解的set方法也可以注入依赖:
@Controller//将对象储存到Spring中
public class UserController {@Autowiredpublic UserController(User user) {this.user = user;}public void say() {user.say();}
}
4.4 三种注入方式的优缺点
4.4.1 属性注入
-
优点:
- 简洁,使用方便。
-
缺点:
- 只能用于IoC容器,非IoC容器不可用,并且只有在使用的时候才会出现空指针异常(NPE)。
- 不能注入一个Final修饰的属性。
4.4.2 构造函数注入
-
优点:
- 可以注入final修饰的属性。
- 注入的对象不会被修改。
- 依赖对象在使用前一定会被完全初始化,因为依赖是在类的构造方法中执行的,而构造方法是在类加载阶段就会执行的方法。
- 通用性好,构造方法是JDK支持的,所以更换任何框架,它都是适用的。
-
缺点:
- 注入多个对象时,代码会比较繁琐。
4.4.3 Setter注入
-
优点:
- 方便在类实例化之后,重新对该对象进行配置或者注入。
-
缺点:
- 不能注入一个Final修饰的属性。
- 注入对象可能会被改变,因为setter方法可能会被多次调用,就有被修改的风险。
根据具体情况和需求,可以选择适合的注入方式来管理对象之间的依赖关系。
4.5 @Autowired存在的问题
当同一类型存在多个Bean时,使用@Autowired 会存在问题:
public class User {String name;public User() {}public User(String name) {this.name = name;}public void say() {System.out.println(name);}
}
@Configuration
public class UserConfig {@Beanpublic User User1() {return new User("a");}@Beanpublic User User2() {return new User("b");}@Beanpublic User User3() {return new User("c");}
}
@Controller//将对象储存到Spring中
public class UserController {@Autowiredpublic void say() {user.say();}
}
项目启动失败,我们在描述这里看到,Spring找到了三个对象 。使用@Autowired注入时,Spring会先根据对象的类型去寻找,如果只找到一个就直接用,如果找到多个,再根据名称(这里我们是user)来寻找,如果没有对应名称就会抛异常。
我们可以使用 @Primary ,@Qualifier,@Resource 这三个注解来解决。
@Primary:指定默认使用哪一个Bean
@Configuration
public class UserConfig {@Primary@Beanpublic User User1() {return new User("a");}@Beanpublic User User2() {return new User("b");}@Beanpublic User User3() {return new User("c");}
}
@Qualifier:指定使用Bean的名称
@Controller//将对象储存到Spring中
public class UserController {@Qualifier("User2")@Autowiredprivate User user;public void say() {user.say();}
}
使用@Resource注入依赖:
@Controller//将对象储存到Spring中
public class UserController {@Resource(name = "User3")private User user;public void say() {user.say();}
}
5. 总结
- Spring: Spring 是一个开源的Java框架,它提供了丰富的功能和组件,用于简化企业级应用程序的开发。Spring 框架提供了依赖注入、面向切面编程、事务管理、数据访问等功能,是一个全面的企业应用开发解决方案。
- Spring MVC: Spring MVC 是 Spring 框架中的一个模块,即Spring功能的一部分,用于构建 Web 应用程序的 MVC(Model-View-Controller)架构。Spring MVC 提供了基于注解的方式来定义控制器、处理请求和渲染视图,使得开发 Web 应用程序更加简单和灵活。
- Spring Boot: Spring Boot 是基于 Spring 框架的快速开发框架,它简化了 Spring 应用程序的搭建和部署过程。Spring Boot 提供了自动配置、约定优于配置、快速启动等特性,使得开发者可以更快速地搭建和运行 Spring 应用程序,而不需要进行繁琐的配置。简单来说Spring Boot 就是对Spring的使用做了一些简化使我们开发效率更高,但其所有的功能都是基于Spring实现的。