目录
IoC是什么?
理解IoC
示例
为什么要使用IOC
DI是什么?
IoC
@Controller(控制器存储)
@Controller的使用
ApplicationContext
@Service(服务存储)
类注解总结
@Bean
方法注解的使用
定义多个对象
Bean的重命名
扫描路径
DI
属性注入
构造方法注入
Setter注入
三种注入方法分析
@Autowired存在的问题
@Primary
@Qualifier
@Resource
Spring可以总结为:包含了众多方法工具的IoC容器
什么是 IoC ?什么是容器?
我们通过本篇文章来进行学习
IoC是什么?
理解IoC
IoC(Inversion of Control):控制反转,即控制权反转,指在传统的程序设计中,流程的控制权通常由程序内部实现,而使用IoC,对象对自己所依赖的资源不再负责获取和管理,而是把这些控制权交给外部容器来实现。
什么是容器?
容器:也就是用来容纳物品的装置(如生活中的箱子)。在计算机中,我们容纳的是应用程序和相关组件,因此,容器是一种用于封装应用程序及其所有相关组件的技术。
什么是控制权反转?
在传统的程序开发过程中, 当我们需要某个对象时,需要自己通过new创建对象,而IoC有专门的容器来创建这些对象,即IoC容器来控制对象的创建,此时,我们可以将创建对象的任务交给容器,程序只需要依赖注入(DI)就可以了。在此过程中,我们获取对象的过程,由主动(自己创建)变为了被动(从IoC容器中获取),控制权颠倒,因此叫做控制反转
我们通过一个例子来进一步理解IoC:
示例
需求:造一辆车
在进行传统程序开发时,我们的思路是这样的:要造一辆车,首先要有轮胎
然后将Tire类主动注入到用Car中
简单代码演示:
Tire:
public class Tire {private int size;public Tire(){this.size = 20;System.out.println("轮胎尺寸:" + size);System.out.println("Tire init");}public void run(){System.out.println("Tire");}
}
Car:
public class Car {private Tire tire;public Car(){tire = new Tire();System.out.println("Car init");}public void run(){System.out.println("Car");}
}
然而,当轮胎的尺寸发生变化,需要通过用户的指定来造轮胎,此时,我们需要修改代码:
对应的,Car也需要进行修改:
此时,修改底层代码,调用该类的代码都需要修改, 程序的耦合性非常高
在上述程序中,我们在造车时根据车轮的大小设计汽车,车轮一修改,汽车的设计就得修改
我们可以换一种思路:我们不需要完成汽车和轮胎的所有设计,将轮胎外包出去,当轮胎的尺寸发生改变时,我们只需向工厂下订单,工厂就会帮我们造好轮胎
在代码中体现为:我们不在类中创建依赖的对象,而是通过传递(也就是注入)的方式
这样,即使Tire中发生改变,Car本身也不需要修改任何代码
底层类发生变化,调用它的类不用做任何代码,也就实现了代码之间的解耦,从而程序设计更加灵活
在传统的代码中,对象创建的顺序是:Car -> Tire
而改进后的代码,对象创建的顺序是:Tire -> Car
我们可以发现:传统开发中,Car控制并创建了Tire,而改进后,控制权发生了反转,不再是由使用方创建并控制依赖对象了,而是将依赖对象注入到对象中,依赖对象的控制权不再由使用方控制
而 IOC有专门的容器来创建这些对象,即IoC容器来控制对象的创建,使用方需要时,只需将依赖注入就可使用依赖对象
为什么要使用IOC
通过上述过程,我们可以看出,使用IoC容器,资源不再由使用资源的双方管理,而是由不使用资源的第三方管理,此时:
1. 能够降低耦合度:IoC容器可以帮助管理对象之间的依赖关系,将对象的创建和管理交给容器来实现,从而降低了使用资源双方的依赖程度,使得代码更易于理解、维护和扩展。
2. 简化配置和管理:IoC容器可以集中管理资源,统一进行配置和管理,减少了对资源的手动管理工作,提高了系统的可维护性和管理效率。
3. 提高了灵活性:通过IoC容器管理对象的依赖关系,可以使系统更加灵活,能够更方便地进行替换、升级或扩展,而不需要修改大量的代码。
DI是什么?
DI(Dependency Injection):依赖注入,在容器运行期间,动态的为应用程序提供运行时所依赖的资源,即程序在运行时需要资源,此时容器就为其提供这个资源
因此,依赖注入可以看做是实现控制反转的一种方式
在示例的改进代码中,是通过构造函数的方式,将依赖对象注入到需要使用的对象中的:
在了解了 IoC 和 DI的基本概念后,我们来学习 Spring IoC 和 DI 的代码实现
Spring 是一个 IoC容器,作为容器,就具备两个功能:存 和 取
Spring容器管理的主要是对象,而这些被管理的对象,我们称之为 Bean。我们将对象交给 Spring 进行管理,由 Spring 来负责对象的创建和销毁,程序只需要告诉 Spring,哪些需要存取,以及如何取
IoC
要将对象交给IoC容器进行管理,需要使用注解,而Spring框架为更好的服务web应用程序,提供了丰富的注解:
类注解:@Controller @Service @Repository @Component @Configuration
方法注解:@Bean
我们先学习类注解
@Controller(控制器存储)
@Controller的使用
使用@Controller存储bean:
@Controllerpublic class UserController {public void hello(){System.out.println("hello...");}
}
如何观察这个对象是否已经存在Spring容器中了呢?
若是能从Spring容器中获取这个对象,则这个对象就已经存在Spring容器中了
我们使用ApplicationContext来帮助我们获取对象
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(DemoApplication.class, args);UserController bean = context.getBean(UserController.class);bean.hello();}
}
我们观察运行结果,发现成功从Spring中获取到Controller对象,且执行了Controller的hello方法:
若我们将@Controller删除,再观察运行结果:
此时程序报错:找不到类型为:com.example.demo.controller.UserController 的bean
ApplicationContext
ApplicationContext是什么呢?
ApplicationContext,即Spring上下文
对象交由Spring进行管理,要从Spring中获取对象,首先要拿到Spring的上下文
我们来理解上下文的概念:
在做语文阅读理解的时候,常会遇见这样的问题:请根据上下文,分析你对...的理解 其中的上下文,指的是文章中与某一词语或文句相连的上文和下文
在学习多线程时,应用进行线程切换的时候,在切换之前会将线程的状态信息暂时存储起来,这里的上下文就包括了当前线程的信息,等下次该线程又得到CPU时间时,就能从上下文中拿到线程上次运行的信息
而在Spring框架中,上下文指的是 Spring IoC 容器管理的对象之间的环境和状态。其中包括 bean 的定义、依赖注入、AOP配置等。
在上述代码中,我们是通过类型(DemoApplication.class)来查找对象的,还有其他的方式获取bean
获取bean的方式
ApplicationContext提供了许多获取bean的方式,而ApplicationContext获取bean对象的功能是父类 BeanFactory 提供的功能
在上述获取bean的方法中,常用的是第1,2,4种
其中涉及到根据名称获取bean,bean的名称是什么呢?
Spring bean 是Spring框架在运行时管理的对象,Spring会给管理的对象起一个名字(例如,学校会给每个学生分配一个学号,根据学号就能够找到对应的学生)
而在Spring中也是如此,为每个对象起一个名字,根据bean的名称,就可以找到对应对象,从而获取对应对象
在分配学号时,学校会根据学生的入学年份、专业、班级等信息分配学号,也就是学号的制定规则
在Spring,是如何命名的呢?Bean的命名约定是什么呢?
命名约定使用Java标准约定作为实例字段名,即 使用小驼峰命名规则
例如:UserController,bean的名称为:userController
根据这个命名规则,我们来获取bean:
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(DemoApplication.class, args);UserController bean = context.getBean(UserController.class);//根据 类型 获取UserController userController = (UserController) context.getBean("userController");//根据 名称 获取UserController userController1 = context.getBean("userController", UserController.class);//根据名称 + 类型 获取System.out.println(bean);System.out.println(userController);System.out.println(userController1);}
}
运行结果:
根据结果我们也可以发现:三种方法获取的对象地址一样,这说明获取的对象是同一个
但是,命名时也有一些特殊情况:
@Controller
public class UController {public void run(){System.out.println("UController");}
}
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(DemoApplication.class, args);UController uController = (UController) context.getBean("uController");}
}
此时,运行结果为:
错误为:没有名称为 uController 的 bean
这是因为,当有多个字符,且第一个和第二个字符都是大写时,要保留原始大小写
因此,UController,bean 的名称为 UController
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(DemoApplication.class, args);UController uController = (UController) context.getBean("UController");}
}
我们来总结一下bean的命名约定:
当有多个字符且第一个和第二个字符都是大写时,保留原始大小写
其他情况,则使用小驼峰命名规则
接下来,我们来看@Service
@Service(服务存储)
使用@Service 存储 bean:
@Service
public class UserService {public void runService(){System.out.println("service...");}
}
获取 bean:
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(DemoApplication.class, args);UserService userService = context.getBean(UserService.class);userService.runService();}
}
@Repository(仓库存储) @Component(组件存储) @Configuration(配置存储)的使用也是类似的,这里就不再一一演示了
类注解总结
为什么要有这么多的类注解呢?
这与 应用分层 类似,为了在看到类注解后,就能直接了解当前类的用途
@Controller:控制层,接收请求,对请求进行处理,并进行响应
@Service:业务逻辑层,处理具体的业务逻辑
@Repository:数据访问层,也称为持久层,处理数据访问操作
@Configuration:配置层,处理项目中的一些配置信息
@Component:泛指组件,当组件不好归类时,可以使用这个注解
类注解之间的关系:
我们观察 @Controller @Service @Repository @Configuration 和 @Component 的源码:
@Controller @Service @Repository @Configuration 这些注解里面都有一个注解 @Component,说明 它们 属于 @Component 的 “子类”
@Component 是一个 元注解,也就是可以注解其他类的注解,@Controller @Service等,这些注解被称为@Component的衍生注解
类注解是添加到某个类上的,但在某些情况下也会出现问题问题:
1. 使用外部包里的类时,没办法添加类注解
2. 一个类需要多个对象时
此时,我们就需要使用方法注解
@Bean
方法注解的使用
@Datapublic class User {private int id;private String name;public User(){}public User(int id, String name) {this.id = id;this.name = name;}
}
public class Users {@Beanpublic User user() {return new User(1, "aaa");}
}
尝试获取:
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(DemoApplication.class, args);User user = context.getBean(User.class);System.out.println(user);}
}
运行结果:
程序报错:找不到 类型为 com.example.demo.model.User 的 bean
为什么会报错呢?
这是因为方法注解要配合类注解使用
我们加上类注解:
@Component
public class Users {@Beanpublic User user() {return new User(1, "aaa");}
}
运行结果:
定义多个对象
若此时同一个类中有多个对象呢?
@Component
public class Users {@Beanpublic User user1() {return new User(1, "aaa");}@Beanpublic User user2(){return new User(2, "bbb");}
}
我们根据类型来获取对象:
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(DemoApplication.class, args);User user = context.getBean(User.class);System.out.println(user);}
}
运行结果:
报错显示:期望只有一个匹配,结果发现了两个:user1,user2
我们可以报错信息中看出:@Bean注解的bean,bean的名称就是它的方法名
我们根据名称来获取bean:
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(DemoApplication.class, args);User user1 = (User) context.getBean("user1");User user2 = (User) context.getBean("user2");System.out.println(user1);System.out.println(user2);}
}
运行结果:
根据 名称 + 类型来获取bean:
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(DemoApplication.class, args);User user1 = (User) context.getBean("user1", User.class);User user2 = (User) context.getBean("user2", User.class);System.out.println(user1);System.out.println(user2);}
}
运行结果:
由此可以看出:@Bean可以针对同一个类定义多个对象
Bean的重命名
可以通过设置name属性对Bean进行重命名:
@Component
public class Users {@Bean(name = {"us1", "user1"})public User user1() {return new User(1, "aaa");}@Beanpublic User user2(){return new User(2, "bbb");}
}
此时就可以使用 us1来获取User对象了:
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(DemoApplication.class, args);User user1 = (User) context.getBean("us1");User user2 = (User) context.getBean("user2", User.class);System.out.println(user1);System.out.println(user2);}
}
运行结果:
其中 name = 可以省略:
@Bean({"us1", "user1"})public User user1() {return new User(1, "aaa");}
而当只有一个名称时,{}也可以省略:
@Bean("us1")public User user1() {return new User(1, "aaa");}
扫描路径
bean想要生效,需要被Spring扫描
我们修改项目工程的目录结构:
此时再运行代码:
程序报错:没有找到名称为us1的bean
为什么没有找到呢?
使用注解声明的bean想要生效需要配置扫描路径,让Spring能够扫描到这些注解
通过 @ComponentScan 来配置扫描路径:
@SpringBootApplication
@ComponentScan({"com.example.demo"})
public class DemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(DemoApplication.class, args);User user1 = (User) context.getBean("us1");User user2 = (User) context.getBean("user2", User.class);System.out.println(user1);System.out.println(user2);}
}
{} 中可以配置多个包路径
为什么前面没有配置 @ComponentScan 注解也能够扫描?
@ComponentScan 虽然没有显示配置,但其实已经包含在启动类声明注解 @SpringBootApplication 中了
其中,扫描的默认范围是 SpringBoot 启动类所在的包及其子包
(在配置类上添加 @ComponentScan 注解,该注解默认会扫描该类所在包下的所有配置类)
因此,将启动类放在我们所希望扫描的包的路径下,这样,定义的bean就可以被扫描到了
DI
在进一步学习了控制反转IoC后,我们来学习依赖注入DI
依赖注入是一个过程,在IoC容器创建bean时,提供运行时所依赖的资源
使用 @Autowired 注解来完成依赖注入
Spring 为我们提供了三种注入方式:
1. 属性注入 (Field Injection)
2. 构造方法注入 (Constructor Injection)
3. Setter注入(Setter Injection)
属性注入
属性注入是通过在类的属性上使用 @Autowired 注解或在配置文件中进行配置,将依赖对象注入到类的属性中
我们将UserService类注入到UserController类中:
UserService:
@Service
public class UserService {public void runService(){System.out.println("service...");}
}
UserController:
@Controller
public class UserController {@Autowiredprivate UserService userService;public void hello(){System.out.println("hello...");userService.runService();}
}
获取UserController中的hello方法:
@SpringBootApplication
@ComponentScan({"com.example.demo"})
public class DemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(DemoApplication.class, args);UserController userController = context.getBean(UserController.class);userController.hello();}
}
运行结果:
若去掉 @Autowired:
程序报错:不能调用 com.example.demo.service.UserService.runService(),因为 this.userService 为空
构造方法注入
构造方法注入是在类的构造方法中实现注入:
@Controller
public class UserController {private UserService userService;@Autowiredpublic UserController(UserService userService){this.userService = userService;}public void hello(){System.out.println("hello...");userService.runService();}
}
运行结果:
若此时类中只有一个构造方法,则 @Autowired 注解可以省略:
@Controller
public class UserController {private UserService userService;// @Autowiredpublic UserController(UserService userService){this.userService = userService;}public void hello(){System.out.println("hello...");userService.runService();}
}
但若类中有多个构造方法,此时就需要添加 @Autowired 注解来明确指定使用哪个构造方法
@Controller
public class UserController {private UserService userService;public UserController(){}@Autowiredpublic UserController(UserService userService){this.userService = userService;}public void hello(){System.out.println("hello...");userService.runService();}
}
若此时去掉 @Autowired 注解,程序报错:
Setter注入
Setter注入与属性的Setter方法类似,只不过需要在设置set方法时添加上 @Autowired 注解:
@Controller
public class UserController {private UserService userService;@Autowiredpublic void setUserService(UserService userService){this.userService = userService;}public void hello(){System.out.println("hello...");userService.runService();}
}
此时,若 去掉 @Autowired:
同样的,程序报错
三种注入方法分析
属性注入:
优点:
简洁方便,可以直接在属性上进行注入,代码简洁,不需要编写额外的构造方法或 Setter 方法
缺点:
1. 只能用于IoC容器
2. 不能注入 final 修饰的属性
构造函数注入:
优点:
1. 可以注入 final 修饰的属性
2. 注入的对象不会被修改
3. 依赖对象在使用前一定会被完全初始化(因为依赖是在类的构造方法中执行的,而构造方法在类加载阶段就会执行)
4. 通用性好,构造方法是JDK支持的,更换框架后也是适用的
缺点:
注入多个对象时,代码量相对较多
Setter注入:
优点:
1. 方便在类实例后,重新对该对象进行配置或注入
2. 可选依赖,可以只提供部分 Setter 方法,不影响其他依赖的注入
缺点:
1. 不能注入 final修改的属性
2. 注入对象可能会发生改变(setter方法可能被多次调用,就有被修改的风险)
3. 需要为每个需要注入的属性编写相应的 Setter 方法,增加了代码量
因此, 选择使用哪种依赖注入方式取决于具体的需求和场景。
@Autowired存在的问题
若同一类型中存在多个bean,此时适用@Autowired会存在问题:
@Controller
public class UserController {@Autowiredprivate User user;public void hello(){System.out.println("hello...");System.out.println(user);;}
}
运行结果:
程序报错:UserController 需要一个 bean,但是发现了两个,即非唯一的bean
如何解决该问题呢?
其实报错信息下面就给出了解决方法:
@Primary
当多个类型相同的bean注入时,加上 @Primary 注解,来确定默认的实现:
@Component
public class Users {@Primary//指定 该 bean 为默认bean实现@Beanpublic User user1() {return new User(1, "aaa");}@Beanpublic User user2(){return new User(2, "bbb");}
}
运行结果:
@Qualifier
使用 @Qualifier 注解,指定当前要注入的对象,在 @Qualifier 的 value属性中,指定注入的 bean的名称,@Qualifier注解不能单独使用,必须配合 @Autowired 使用
@Controller
public class UserController {@Qualifier("user2")@Autowiredprivate User user;public void hello(){System.out.println("hello...");System.out.println(user);;}
}
运行结果:
@Resource
@Resource注解,是按照 bean 的名称进行注入,通过 name 属性指定要注入的 bean 的名称
@Controller
public class UserController {@Resource(name = "user1")private User user;public void hello(){System.out.println("hello...");System.out.println(user);;}
}
运行结果:
@Autowired 与 @Resource 的区别
@Autowired 是 Spring 框架提供的注解,而 @Resource 是JDK 提供的注解
@Autowired 默认是按照类型注入的,而 @Resource 是按照名称注入的,相比于 @Autowired,@Resource 支持更多的参数设置