Spring IoC:解耦与控制反转的艺术
目录
- Spring IoC 从初识到精通
- 一、IoC(控制反转)
- 二、Bean的存储
- 2.1 `@Controller`(控制器存储)
- 2.2 `@Service` 服务存储
- 2.3 `@Repository`(仓库存储)
- 2.4 `@Component`(组件存储)
- 2.5`@Configuration`
- 三、为什么要这么多的注解?
- 四、 ` @Bean` 方法注解
- 五、总结
Spring IoC 从初识到精通
一、IoC(控制反转)
其实IoC我们在前面已经使用了, 我们在前面讲到, 在类上面添加@RestController 和@Controller 注解, 就是把这个对象交给Spring管理, Spring 框架启动时就会加载该类. 把对象交给Spring管理, 就是IoC思想。
接下来我们通过案例来了解一下什么是IoC
需求: 造⼀辆车:
传统程序开发
我们的实现思路是这样的:
先设计轮子(Tire),然后根据轮子的大小设计底盘(Bottom),接着根据底盘设计车身(Framework),最后根据车身设计好整个汽车(Car)。这里就出现了一个"依赖"关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。
如图所示:
public class NewCar {
public static void main(String[] args) {
Car car=new Car();
car.run();
}
static class Car {
private Framework framework;
public Car(){
framework=new Framework();
System.out.println("car...init...");
}
void run(){
System.out.println("car....run...");
}
}
static class Framework {
private Bottom bottom;
public Framework(){
bottom=new Bottom();
System.out.println("car....bottom..");
}
static class Bottom {
private Tire tire;
public Bottom(){
tire=new Tire();
System.out.println("car...tire");
}
static class Tire {
private int size;
public Tire() {
this.size = 17;
System.out.println("轮胎尺寸" + size);
}
}
}
}
}
这样的设计看起来没问题,但是可维护性却很低.
接下来需求有了变更: 随着对的车的需求量越来越大,个性化需求也会越来越多,我们需要加工多种尺寸的轮胎。
那这个时候就要对上面的程序进行修改了,修改后的代码如下所示:
public class NewCar {
public static void main(String[] args) {
Car car=new Car(50);
car.run();
}
static class Car {
private Framework framework;
public Car(int size){
framework=new Framework(30);
System.out.println("car...init...");
}
void run(){
System.out.println("car....run...");
}
}
static class Framework {
private Bottom bottom;
public Framework(int size){
bottom=new Bottom(20);
System.out.println("car....bottom..");
}
static class Bottom {
private Tire tire;
public Bottom(int size){
tire=new Tire(25);
System.out.println("car...tire");
}
static class Tire {
private int size;
public Tire(int size) {
this.size = 17;
System.out.println("轮胎尺寸" + size);
}
}
}
}
}
从以上代码可以看出,以上程序的问题是:当最底层代码改动之后,整个调用链上的所有代码都需要修改。
程序的耦合度非常高(修改⼀处代码, 影响其他处的代码修改)。
解决方案
在上⾯的程序中, 我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改. 同样因为我们是根据底盘设计的车身,那么车身也得改,同理汽车设计也得改, 也就是整个设计几乎都得改
我们尝试换⼀种思路, 我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子. 这时候,依赖关系就倒置过来了:轮子依赖底盘, 底盘依赖车身,车身依赖汽车这就类似我们打造⼀辆完整的汽车, 如果所有的配件都是自己造,那么当客户需求发生改变的时候,比如轮胎的尺寸不再是原来的尺寸了,那我们要自己动手来改了,但如果我们是把轮胎外包出去,那么即使是轮胎的尺寸发生改变了,我们只需要向代理工厂下订单就行了,我们自身是不需要出力的。
如何来实现呢:
我们可以尝试不在每个类中自己创建下级类,如果自己创建下级类就会出现当下级类发生改变操作,自己也要跟着修改。
此时,我们只需要将原来由自己创建的下级类,改为传递的方式(也就是注入的方式),因为我们不需要在当前类中创建下级类了,所以下级类即使发生变化(创建或减少参数),当前类本身也无需修改任何代码,这样就完成了程序的解耦。
修改后的代码:
public class NewCar {
public static void main(String[] args) {
Tire tire=new Tire(20);
Bottom bottom=new Bottom(tire);
Framework framework=new Framework(bottom);
Car car=new Car(framework);
car.run();
}
static class Car {
private Framework framework;
public Car(Framework framework){
this.framework=framework;
System.out.println("car...init...");
}
void run(){
System.out.println("car....run...");
}
}
static class Framework {
private Bottom bottom;
public Framework(Bottom bottome) {
this.bottom = bottome;
System.out.println("car....bottom..");
}
}
static class Bottom {
private Tire tire;
public Bottom(Tire tire) {
this.tire = tire;
System.out.println("car...tire");
}
}
static class Tire {
private int size;
public Tire(int size) {
this.size = 17;
System.out.println("轮胎尺寸" + size);
}
}
}
代码经过以上调整,无论底层类如何变化,整个调用链是不用做任何改变的,这样就完成了代码之间的解耦,从而实现了更加灵活、通用的程序设计。
IoC 优势
在传统的代码中对象创建顺序是:Car -> Framework -> Bottom -> Tire
改进之后解耦的代码的对象创建顺序是:Tire -> Bottom -> Framework -> Car
我们发现了⼀个规律,通用程序的实现代码,类的创建顺序是反的,传统代码是 Car 控制并创建了Framework,Framework 创建并创建了Bottom,依次往下,而改进之后的控制权发生的反转,不再是使用方对对象创建并控制依赖对象了,而是把依赖对象注入将当前对象中,依赖对象的控制权不再由当前类控制了.
这样的话, 即使依赖类发生任何改变,当前类都是不受影响的,这就是典型的控制反转,也就是 IoC 的实现思想。
到这里, 我们⼤概就知道了什么是控制反转了, 那什么是控制反转容器呢, 也就是IoC容器。
这部分代码, 就是IoC容器做的工作.
从上面也可以看出来, IoC容器具备以下优点:
资源不由使用资源的双方管理,而由不使用资源的第三方管理,这可以带来很多好处。第一,资源集中管理,实现资源的可配置和易管理。第二,降低了使用资源双方的依赖程度,也就是我们说的耦合度。
- 资源集中管理: IoC容器会帮我们管理⼀些资源(对象等), 我们需要使用时, 只需要从IoC容器中去取就可以了。
- 我们在创建实例的时候不需要了解其中的细节, 降低了使用资源双方的依赖程度, 也就是耦合度.Spring 就是⼀种IoC容器, 帮助我们来做了这些资源管理。
前面我们提到IoC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。也就是bean的存储。
二、Bean的存储
在之前的案例中,要把某个对象交给IOC容器管理,需要在类上添加⼀个注解: @Component而Spring框架为了更好的服务web应用程序, 提供了更丰富的注解。
共有两类注解类型可以实现:
1. 类注解:@Controller
、@Service
、@Repository
、@Component
、@Configuration
.
2. 方法注解:@Bean
.
接下来我们分别来看:
2.1 @Controller
(控制器存储)
使用 @Controller
存储 bean 的代码如下所示:
@Controller
public class HelloController {
public void print(){
System.out.println("HelloController!!!");
}
}
如何观察这个对象已经存在Spring容器当中了呢?
接下来我们学习如何从Spring容器中获取对象:
先学习一下Bean的命名约束:
Bean的命名约束
:
程序开发⼈员不需要为bean指定名称(BeanId), 如果没有显式的提供名称(BeanId),Spring容器将为该bean生成唯⼀的名称.
命名约定使用Java标准约定作为实例字段名. 也就是说,bean名称以小写字母开头,然后使用驼峰式大小写。
比如
类名: UserController, Bean的名称为: userController
类名: AccountManager, Bean的名称为: accountManager
类名: AccountService, Bean的名称为: accountService
也有⼀些特殊情况, 当有多个字符并且第⼀个和第⼆个字符都是⼤写时, 将保留原始的大小写. 这些规则与java.beans.Introspector.decapitalize (Spring在这里使用的)定义的规则相同.
比如
类名: UController, Bean的名称为: UController
类名: AManager,Bean的名称为:AManager
根据这个命名规则我们来获取Bean:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//上下文管理器
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
HelloController bean1 = context.getBean(HelloController.class);
bean1.print();
}
}
获取Bean的方法很多,这里就不一个一个写了。
2.2 @Service
服务存储
@Service
public class UserService {;
public void print(){
System.out.println("do Service");
}
}
获取Bean:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserService bean=context.getBean(UserService.class);
bean.print();
}
}
2.3 @Repository
(仓库存储)
使用@Repository
的方法存储bean:
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository {
public void print(){
System.out.println("UserRepository!!!");
}
}
获取Bean:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserRepository bean=context.getBean(UserRepository.class);
bean.print();
}
}
2.4 @Component
(组件存储)
使用@Component
的方法存储Bean:
@Configuration
public class UserComponent {
public void print(){
System.out.println("do UserCompoent!");
}
}
获取Bean:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserRepository UserComponent bean1 = context.getBean(UserComponent.class);
bean1.print();
}
}
2.5@Configuration
使用@Configuration
的方法存储Bean
@Configuration
public class UserConfig {
public void print(){
System.out.println("do config");
}
}
获取Bean:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserConfig bean1 = context.getBean(UserConfig.class);
bean1.print();
}
}
三、为什么要这么多的注解?
这个也是和咱们前⾯讲的应用分层是呼应的. 让程序员看到类注解之后,就能直接了解当前类的用途。
@Controller:控制层, 接收请求, 对请求进行处理, 并进行响应。
• @Servie:业务逻辑层, 处理具体的业务逻辑。
• @Repository:数据访问层,也称为持久层. 负责数据访问操作。
• @Configuration:配置层. 处理项目中的⼀些配置信息。
查看 @Controller / @Service / @Repository / @Configuration
等注解的源码发现:
其实这些注解里面都有⼀个注解 @Component ,说明它们本⾝就是属于 @Component 的"子类".
@Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller , @Service ,@Repository 等. 这些注解被称为 @Component 的衍生注解.
@Controller , @Service 和 @Repository ⽤于更具体的⽤例(分别在控制层, 业务逻辑层, 持久化层), 在开发过程中, 如果你要在业务逻辑层使⽤ @Component 或@Service,显然@Service是更好的选择。
四、 @Bean
方法注解
类注解是添加到某个类上的, 但是存在两个问题:
- 使用外部包里的类, 没办法添加类注解
- 一个类, 需要多个对象, 比如多个数据源
这种场景, 我们就需要使用方法注解 @Bean
我们先来看看方法注解如何使用:
@Data
public class Student {
public String name;
public int age;
}
public class StudentComponent {
@Bean
public Student s1(){
Student student=new Student();
student.setAge(18);
student.setName("张三");
return student;
}
}
获取Bean对象的student:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//上下文管理器
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
Student student =context.getBean(Student.class);
//使用对象
System.out.println(student);
}
}
这里显示没有找到,那是什么原因?
在 Spring 框架的设计中,方法注解 @Bean 要配合类注解才能将对象正常的存储到 Spring 容器中
如下代码所示:
@Component
public class StudentComponent {
@Bean
public Student s1(){
Student student=new Student();
student.setAge(18);
student.setName("张三");
return student;
}
}
可以看到获取Bean对象当中的student成功了。
定义多个对象
对于同一个类, 如何定义多个对象呢?
比如多数据源的场景, 类是同一个, 但是配置不同, 指向不同的数据源。
代码:
@Component
public class StudentComponent {
public Student s1(){
Student student=new Student();
student.setAge(18);
student.setName("张三");
return student;
}
@Bean
public Student s2(){
Student student=new Student();
student.setAge(20);
student.setName("李四");
return student;
}
}
获取Bean对象的信息:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//上下文管理器
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
Student student =context.getBean(Student.class);
//使用对象
System.out.println(student);
}
}
只希望匹配一个但是看到找到了两个,然后报错了,分不清student是哪个,那怎么解决呢?
@Primary
注解 指定默认的bean
把上面的代码修改一下:
@Component
public class StudentComponent {
@Primary
public Student s1(){
Student student=new Student();
student.setAge(18);
student.setName("张三");
return student;
}
@Bean
public Student s2(){
Student student=new Student();
student.setAge(20);
student.setName("李四");
return student;
}
}
可以看到成功了!!!
重命名 Bean
@Configuration
public class AppConfig {
@Bean(name = "customUserService")
public UserService userService() {
return new UserService();
}
}
对userService
进行重命名。
五、总结
总结语:
Spring IoC(控制反转)是Spring框架的核心思想之一,它通过将对象的创建和管理权交给Spring容器,实现了代码的解耦与灵活性。IoC不仅简化了开发流程,还提高了系统的可测试性和可维护性。通过控制反转,开发者可以更专注于业务逻辑的实现,而不是对象的创建与管理。掌握Spring IoC的思想与实践,是每一位Spring开发者必须具备的核心能力,也是构建高质量、可扩展应用的重要基石。
希望本文能帮助你理解 Spring IoC 的核心概念,并在实际项目中灵活运用!