1. IoC & DI ⼊⻔
1.1 Spring 是什么?
Spring是⼀个开源框架, 他让我们的开发更加简单. 他⽀持⼴泛的应⽤场
景, 有着活跃⽽庞⼤的社区, 这也是Spring能够⻓久不衰的原因
Spring 是包含了众多⼯具⽅法的 IoC 容器
1.1.1 什么是容器?
容器是⽤来容纳某种物品的(基本)装置。
• List/Map -> 数据存储容器
• Tomcat -> Web 容器
1.1.2 什么是 IoC?
在类上⾯添加 @RestController 和 @Controller 注解, 就是把这个对象交给Spring管理, Spring 框架启动时就会加载该类. 把对象交 给Spring管理, 就是IoC思想.
IoC: Inversion of Control (控制反转), 也就是说 Spring 是⼀个"控制反转"的容器.
1.2 IoC 介绍
接下来我们通过案例来了解⼀下什么是IoC
需求: 造⼀辆⻋
1.2.1 传统程序开发
我们的实现思路是这样的:
先设计轮⼦(Tire),然后根据轮⼦的⼤⼩设计底盘(Bottom),接着根据底盘设计⻋⾝(Framework),最 后根据⻋⾝设计好整个汽⻋(Car)。这⾥就出现了⼀个"依赖"关系:汽⻋依赖⻋⾝,⻋⾝依赖底盘,底 盘依赖轮⼦.
1 public class NewCarExample {
2 public static void main(String[] args) {
3 Car car = new Car();
4 car.run();
5 }
6
7 /**
8 * 汽⻋对象
9 */
10 static class Car {
11 private Framework framework;
12
13 public Car() {
14 framework = new Framework();
15 System.out.println("Car init....");
16 }
17 public void run(){
18 System.out.println("Car run...");
19 }
20 }
21
22 /**
23 * ⻋⾝类
24 */
25 static class Framework {
26 private Bottom bottom;
27
28 public Framework() {
29 bottom = new Bottom();
30 System.out.println("Framework init...");
31 }
32 }
33
34 /**
35 * 底盘类
36 */
37 static class Bottom {
38 private Tire tire;
39
40 public Bottom() {
41 this.tire = new Tire();
42 System.out.println("Bottom init...");
43 }
44 }
45
46 /**
47 * 轮胎类
48 */
49 static class Tire {
50 // 尺⼨
51 private int size;
52
53 public Tire(){
54 this.size = 17;
55 System.out.println("轮胎尺⼨:" + size);
56 }
57 }
58 }
1.2.2 问题分析
程序的耦合度⾮常⾼(修改⼀处代码, 影响其他处的代码修改)
修改之后, 其他调⽤程序也会报错, 我们需要继续修改
1.2.3 解决⽅案
先设计汽⻋的⼤概样⼦,然后根据汽⻋的样⼦来设计⻋⾝,根据⻋⾝来设计 底盘,最后根据底盘来设计轮⼦. 这时候,依赖关系就倒置过来了:轮⼦依赖底盘, 底盘依赖⻋⾝, ⻋⾝依赖汽⻋
此时,我们只需要将原来由⾃⼰创建的下级类,改为传递的⽅式(也就是注⼊的⽅式),因为我们不 需要在当前类中创建下级类了,所以下级类即使发⽣变化(创建或减少参数),当前类本⾝也⽆需修 改任何代码,这样就完成了程序的解耦.
1.2.4 IoC程序开发
1 public class IocCarExample {
2 public static void main(String[] args) {
3 Tire tire = new Tire(20);
4 Bottom bottom = new Bottom(tire);
5 Framework framework = new Framework(bottom);
6 Car car = new Car(framework);
7 car.run();
8 }
9
10 static class Car {
11 private Framework framework;
12
13 public Car(Framework framework) {
14 this.framework = framework;
15 System.out.println("Car init....");
16 }
17 public void run() {
18 System.out.println("Car run...");
19 }
20 }
21
22 static class Framework {
23 private Bottom bottom;
24
25 public Framework(Bottom bottom) {
26 this.bottom = bottom;
27 System.out.println("Framework init...");
28 }
29 }
30
31 static class Bottom {
32 private Tire tire;
33
34 public Bottom(Tire tire) {
35 this.tire = tire;
36 System.out.println("Bottom init...");
37 }
38 }
39
40 static class Tire {
41 private int size;
42
43 public Tire(int size) {
44 this.size = size;
45 System.out.println("轮胎尺⼨:" + size);
46 }
47 }
48 }
优点:资源不由使⽤资源的双⽅管理,⽽由不使⽤资源的第三⽅管理
1. 资源集中管理: IoC容器会帮我们管理⼀些资源(对象等), 我们需要使⽤时, 只需要从IoC容器中去取
就可以了
2. 我们在创建实例的时候不需要了解其中的细节, 降低了使⽤资源双⽅的依赖程度, 也就是耦合度.
1.3 DI 介绍
DI: Dependency Injection(依赖注⼊)
容器在运⾏期间, 动态的为应⽤程序提供运⾏时所依赖的资源,称之为依赖注⼊。
上述代码为例
2. IoC & DI 使⽤
既然 Spring 是⼀个 IoC(控制反转)容器,作为容器, 那么它就具备两个最基础的功能:
• 存
• 取
Spring 容器 管理的主要是对象, 这些对象, 我们称之为"Bean". 我们把这些对象交由Spring管理, 由 Spring来负责对象的创建和销毁. 我们程序只需要告诉Spring, 哪些需要存, 以及如何从Spring中出 对象
3. IoC 详解
IoC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对
象,也就是bean的存储
3.1 Bean的存储
Spring框架为了更好的服务web应⽤程序, 提供了更丰富的注解.
共有两类注解类型可以实现:
1. 类注解:@Controller、@Service、@Repository、@Component、@Configuration.
2. ⽅法注解:@Bean
3.1.1 @Controller(控制器存储)
使⽤ @Controller 存储 bean 的代码如下所⽰:
1 @Controller // 将对象存储到 Spring 中
2 public class UserController {
3 public void sayHi(){
4 System.out.println("hi,UserController...");
5 }
6 }
从Spring容器中获取对象
1 @SpringBootApplication
2 public class SpringIocDemoApplication {
3
4 public static void main(String[] args) {
5 //获取Spring上下⽂对象
6 ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
7 //从Spring上下⽂中获取对象
8 UserController userController = context.getBean(UserController.class);
9 //使⽤对象
10 userController.sayHi();
11 }
12 }
因为对象都交给 Spring 管理了,所以获取对象要从 Spring 中获取,那么就得先得到 Spring 的上下 ⽂
这个上下⽂, 就是指当前的运⾏环境, 也可以看作是⼀个容器, 容器⾥存了很多内容, 这些内容是当前 运⾏的环境
获取bean对象的其他⽅式
1 public interface BeanFactory {
2
3
4
5 // 1. 根据bean名称获取bean
6 Object getBean(String var1) throws BeansException;
7 // 2. 根据bean名称和类型获取bean
8 <T> T getBean(String var1, Class<T> var2) throws BeansException;
9 // 3. 按bean名称和构造函数参数动态创建bean,只适⽤于具有原型(prototype)作⽤域的bean
10 Object getBean(String var1, Object... var2) throws BeansException;
11 // 4. 根据类型获取bean
12 <T> T getBean(Class<T> var1) throws BeansException;
13 // 5. 按bean类型和构造函数参数动态创建bean, 只适⽤于具有原型(prototype)作⽤域的bean
14 <T> T getBean(Class<T> var1, Object... var2) throws BeansException;
15
16
17 }
Bean 命名约定
1.Spring bean是Spring框架在运⾏时管理的对象, Spring会给管理的对象起⼀个名字.
2.程序开发⼈员不需要为bean指定名称(BeanId), 如果没有显式的提供名称(BeanId),Spring容器将为该 bean⽣成唯⼀的名称
3.命名约定使⽤Java标准约定作为实例字段名. 也就是说,bean名称以⼩写字⺟开头,然后使⽤驼峰式 ⼤⼩写.
⽐如
类名: UserController, Bean的名称为: userController
类名: AccountManager, Bean的名称为: accountManager
类名: AccountService, Bean的名称为: accountService
根据这个命名规则, 我们来获取Bean
1 @SpringBootApplication
2 public class SpringIocDemoApplication {
3
4 public static void main(String[] args) {
5 //获取Spring上下⽂对象
6 ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
7 //从Spring上下⽂中获取对象
8 //根据bean类型, 从Spring上下⽂中获取对象
9 UserController userController1 = context.getBean(UserController.class);
10 //根据bean名称, 从Spring上下⽂中获取对象
11 UserController userController2 = (UserController) context.getBean("userCon
12 //根据bean类型+名称, 从Spring上下⽂中获取对象
13 UserController userController3 = context.getBean("userController",UserCont
14
15 System.out.println(userController1);
16 System.out.println(userController2);
17 System.out.println(userController3);
18 }
19 }
3.1.2 @Service(服务存储)
使⽤ @Service 存储 bean 的代码如下所⽰:
1 @Service
2 public class UserService {
3 public void sayHi(String name) {
4 System.out.println("Hi," + name);
5 }
6 }
读取 bean 的代码:
1 @SpringBootApplication
2 public class SpringIocDemoApplication {
3
4 public static void main(String[] args) {
5 //获取Spring上下⽂对象
6 ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
7 //从Spring中获取UserService对象
8 UserService userService = context.getBean(UserService.class);
9 //使⽤对象
10 userService.sayHi();
11 }
}
3.1.3 @Repository(仓库存储)
使⽤ @Repository 存储 bean 的代码如下所⽰:
1 @Repository
2 public class UserRepository {
3 public void sayHi() {
4 System.out.println("Hi, UserRepository~");
5 }
6 }
读取 bean 的代码:
1 @SpringBootApplication
2 public class SpringIocDemoApplication {
3
4 public static void main(String[] args) {
5 //获取Spring上下⽂对象
6 ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
7 //从Spring上下⽂中获取对象
8 UserRepository userRepository = context.getBean(UserRepository.class);
9 //使⽤对象
10 userRepository.sayHi();
11 }
12 }
3.1.4 @Component(组件存储)
使⽤ @Component 存储 bean 的代码如下所⽰:
1 @Component
2 public class UserComponent {
3 public void sayHi() {
4 System.out.println("Hi, UserComponent~");
5 }
6 }
读取 bean 的代码:
1 @SpringBootApplication
2 public class SpringIocDemoApplication {
3
4 public static void main(String[] args) {
5 //获取Spring上下⽂对象
6 ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
7 //从Spring上下⽂中获取对象
8 UserComponent userComponent = context.getBean(UserComponent.class);
9 //使⽤对象
10 userComponent.sayHi();
11 }
12 }
3.1.5 @Configuration(配置存储)
使⽤ @Configuration 存储 bean 的代码如下所⽰:
1 @Configuration
2 public class UserConfiguration {
3 public void sayHi() {
4 System.out.println("Hi,UserConfiguration~");
5 }
6 }
读取 bean 的代码:
1 @SpringBootApplication
2 public class SpringIocDemoApplication {
3
4 public static void main(String[] args) {
5 //获取Spring上下⽂对象
6 ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
7 //从Spring上下⽂中获取对象
8 UserConfiguration userConfiguration = context.getBean(UserConfiguration.cl
9 //使⽤对象
10 userConfiguration.sayHi();
11 }
12 }
3.2 为什么要这么多类注解?
这个也是和应⽤分层是呼应的. 让程序员看到类注解之后,就能直接了解当前类的⽤途.
• @Controller:控制层, 接收请求, 对请求进⾏处理, 并进⾏响应.
• @Servie:业务逻辑层, 处理具体的业务逻辑.
• @Repository:数据访问层,也称为持久层. 负责数据访问操作
• @Configuration:配置层. 处理项⽬中的⼀些配置信息
程序的应⽤分层,调⽤流程如下:
3.3 ⽅法注解 @Bean
类注解是添加到某个类上的, 但是存在两个问题:
1. 使⽤外部包⾥的类, 没办法添加类注解
2. ⼀个类, 需要多个对象, ⽐如多个数据源
3.3.1 ⽅法注解要配合类注解使⽤
在 Spring 框架的设计中,⽅法注解 @Bean 要配合类注解才能将对象正常的存储到 Spring 容器中
1 @Component
2 public class BeanConfig {
3 @Bean
4 public User user(){
5 User user = new User();
6 user.setName("zhangsan");
7 user.setAge(18);
8 return user;
9 }
10 }
1 @SpringBootApplication
2 public class SpringIocDemoApplication {
3
4 public static void main(String[] args) {
5 //获取Spring上下⽂对象
6 ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
7 //从Spring上下⽂中获取对象
8 User user = context.getBean(User.class);
9 //使⽤对象
10 System.out.println(user);
11 }
12 }
3.3.2 定义多个对象
对于同⼀个类, 如何定义多个对象呢?
⽐如多数据源的场景, 类是同⼀个, 但是配置不同, 指向不同的数据源
1 @Component
2 public class BeanConfig {
3 @Bean
4 public User user1(){
5 User user = new User();
6 user.setName("zhangsan");
7 user.setAge(18);
8 return user;
9 }
10
11 @Bean
12 public User user2(){
13 User user = new User();
14 user.setName("lisi");
15 user.setAge(19);
16 return user;
17 }
18 }
定义了多个对象的话, 我们根据类型获取对象, 获取的是哪个对象呢?
1 @SpringBootApplication
2 public class SpringIocDemoApplication {
3
4 public static void main(String[] args) {
5 //获取Spring上下⽂对象
6 ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
7 //从Spring上下⽂中获取对象
8 User user = context.getBean(User.class);
9 //使⽤对象
10 System.out.println(user);
11 }
12 }
报错信息显⽰: 期望只有⼀个匹配, 结果发现了两个, user1, user2
从报错信息中, 可以看出来, @Bean 注解的bean, bean的名称就是它的⽅法名
接下来我们根据名称来获取bean对象
1 @SpringBootApplication
2 public class SpringIocDemoApplication {
3
4 public static void main(String[] args) {
5 //获取Spring上下⽂对象
6 ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
7 //根据bean名称, 从Spring上下⽂中获取对象
8 User user1 = (User) context.getBean("user1");
9 User user2 = (User) context.getBean("user2");
10 System.out.println(user1);
11 System.out.println(user2);
12 }
13 }
结果:
可以看到, @Bean 可以针对同⼀个类, 定义多个对象.
3.3.3 重命名 Bean
可以通过设置 name 属性给 Bean 对象进⾏重命名操作
1 @Bean(name = {"u1","user1"})
2 public User user1(){
3 User user = new User();
4 user.setName("zhangsan");
5 user.setAge(18);
6 return user;
7 }
此时我们使⽤ u1 就可以获取到 User 对象了,如下代码所⽰:
1 @SpringBootApplication
2 public class SpringIocDemoApplication {
3
4 public static void main(String[] args) {
5 //获取Spring上下⽂对象
6 ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
7 //从Spring上下⽂中获取对象
8 User u1 = (User) context.getBean("u1");
9 //使⽤对象
10 System.out.println(u1);
11 }
12 }
name={} 可以省略,如下代码所⽰:
1 @Bean({"u1","user1"})
2 public User user1(){
3 User user = new User();
4 user.setName("zhangsan");
5 user.setAge(18);
6 return user;
7 }
只有⼀个名称时, {}也可以省略, 如:
1 @Bean("u1")
2 public User user1(){
3 User user = new User();
4 user.setName("zhangsan");
5 user.setAge(18);
6 return user;
7 }
3.4 扫描路径
Q: 使⽤前⾯学习的四个注解声明的bean,⼀定会⽣效吗?
A: 不⼀定(原因:bean想要⽣效,还需要被Spring扫描)
下⾯我们通过修改项⽬⼯程的⽬录结构,来测试bean对象是否⽣效:
再运⾏代码:
1 @SpringBootApplication
2 public class SpringIocDemoApplication {
3
4 public static void main(String[] args) {
5 //获取Spring上下⽂对象
6 ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
7 //从Spring上下⽂中获取对象
8 User u1 = (User) context.getBean("u1");
9 //使⽤对象
10 System.out.println(u1);
11 }
12 }
解释: 没有bean的名称为u1
为什么没有找到bean对象呢?
使⽤五⼤注解声明的bean,要想⽣效, 还需要配置扫描路径, 让Spring扫描到这些注解
也就是通过 @ComponentScan 来配置扫描路径.
那为什么前⾯没有配置 @ComponentScan注解也可以呢?
@ComponentScan 注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解
@SpringBootApplication 中了
默认扫描的范围是SpringBoot启动类所在包及其⼦包
推荐作法
把启动类放在我们希望扫描的包的路径下, 这样我们定义的bean就都可以被扫描到
4. DI 详解
依赖注⼊是⼀个过程,是指IoC容器在创建Bean时, 去提供运⾏时所依赖的资源,⽽资源指的就是对象.,使⽤了 @Autowired 这个注解,完成了依赖注⼊的操作
关于依赖注⼊, Spring也给我们提供了三种⽅式:
1. 属性注⼊(Field Injection)
2. 构造⽅法注⼊(Constructor Injection)
3. Setter 注⼊(Setter Injection)
4.1 属性注⼊
将 Service 类注⼊到 Controller 类中.
Service 类的实现代码如下:
1 import org.springframework.stereotype.Service;
2
3 @Service
4 public class UserService {
5 public void sayHi() {
6 System.out.println("Hi,UserService");
7 }
8 }
Controller 类的实现代码如下:
1 @Controller
2 public class UserController {
3 //注⼊⽅法1: 属性注⼊
4 @Autowired
5 private UserService userService;
6
7 public void sayHi(){
8 System.out.println("hi,UserController...");
9 userService.sayHi();
10 }
11 }
获取 Controller 中的 sayHi⽅法:
1 @SpringBootApplication
2 public class SpringIocDemoApplication {
3
4 public static void main(String[] args) {
5 //获取Spring上下⽂对象
6 ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
7 //从Spring上下⽂中获取对象
8 UserController userController = (UserController) context.getBean("userCont
9 //使⽤对象
10 userController.sayHi();
11 }
12 }
结果
4.2 构造⽅法注⼊
构造⽅法注⼊是在类的构造⽅法中实现注⼊,如下代码所⽰:
1 @Controller
2 public class UserController2 {
3 //注⼊⽅法2: 构造⽅法
4 private UserService userService;
5
6 @Autowired
7 public UserController2(UserService userService) {
8 this.userService = userService;
9 }
10
11 public void sayHi(){
12 System.out.println("hi,UserController2...");
13 userService.sayHi();
14 }
15 }
注意事项:如果类只有⼀个构造⽅法,那么 @Autowired 注解可以省略;如果类中有多个构造⽅法, 那么需要添加上 @Autowired 来明确指定到底使⽤哪个构造⽅法
4.3 Setter 注⼊
Setter 注⼊和属性的 Setter ⽅法实现类似,只不过在设置 set ⽅法的时候需要加上 @Autowired 注
解 ,如下代码所⽰:
1 @Controller
2 public class UserController3 {
3 //注⼊⽅法3: Setter⽅法注⼊
4 private UserService userService;
5
6 @Autowired
7 public void setUserService(UserService userService) {
8 this.userService = userService;
9 }
10
11 public void sayHi(){
12 System.out.println("hi,UserController3...");
13 userService.sayHi();
14 }
15 }
4.4 三种注⼊优缺点分析
• 属性注⼊
◦ 优点: 简洁,使⽤⽅便;
◦ 缺点:
▪ 只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只有在使⽤的时候才会出现 NPE(空指
针异常)
▪ 不能注⼊⼀个Final修饰的属性
• 构造函数注⼊(Spring 4.X推荐)
◦ 优点:
▪ 可以注⼊final修饰的属性
▪ 注⼊的对象不会被修改
▪ 依赖对象在使⽤前⼀定会被完全初始化,因为依赖是在类的构造⽅法中执⾏的,⽽构造⽅法
是在类加载阶段就会执⾏的⽅法.
▪ 通⽤性好, 构造⽅法是JDK⽀持的, 所以更换任何框架,他都是适⽤的
◦ 缺点:
▪ 注⼊多个对象时, 代码会⽐较繁琐
• Setter注⼊(Spring 3.X推荐)
◦ 优点: ⽅便在类实例之后, 重新对该对象进⾏配置或者注⼊
◦ 缺点:
▪ 不能注⼊⼀个Final修饰的属性
▪ 注⼊对象可能会被改变, 因为setter⽅法可能会被多次调⽤, 就有被修改的⻛险
4.5 @Autowired存在问题
当同⼀类型存在多个bean时, 使⽤@Autowired会存在问题
1 @Component
2 public class BeanConfig {
3
4 @Bean("u1")
5 public User user1(){
6 User user = new User();
7 user.setName("zhangsan");
8 user.setAge(18);
9 return user;
10 }
11
12 @Bean
13 public User user2() {
14 User user = new User();
15 user.setName("lisi");
16 user.setAge(19);
17 return user;
18 }
19 }
1 @Controller
2 public class UserController {
3
4 @Autowired
5 private UserService userService;
6 //注⼊user
7 @Autowired
8 private User user;
9
10 public void sayHi(){
11 System.out.println("hi,UserController...");
12 userService.sayHi();
13 System.out.println(user);
14 }
15 }
结果:
报错的原因是,⾮唯⼀的 Bean 对象(当容器中存在多个同一类型的 Bean 时,Spring 无法确定应该注入哪一个 Bean 到目标对象中)
如何解决上述问题呢?Spring提供了以下⼏种解决⽅案:
• @Primary
• @Qualifier
• @Resource
使⽤@Primary注解:当存在多个相同类型的Bean注⼊时,加上@Primary注解,来确定默认的实现
1 @Component
2 public class BeanConfig {
3
4 @Primary //指定该bean为默认bean的实现
5 @Bean("u1")
6 public User user1(){
7 User user = new User();
8 user.setName("zhangsan");
9 user.setAge(18);
10 return user;
11 }
12
13 @Bean
14 public User user2() {
15 User user = new User();
16 user.setName("lisi");
17 user.setAge(19);
18 return user;
19 }
20 }
使⽤@Qualifier注解:指定当前要注⼊的bean对象。 在@Qualifier的value属性中,指定注⼊的bean 的名称。
• @Qualifier注解不能单独使⽤,必须配合@Autowired使⽤
1 @Controller
2 public class UserController {
3 @Qualifier("user2") //指定bean名称
4 @Autowired
5 private User user;
6
7 public void sayHi(){
8 System.out.println("hi,UserController...");
9 System.out.println(user);
10 }
11 }
使⽤@Resource注解:是按照bean的名称进⾏注⼊。通过name属性指定要注⼊的bean的名称。
1 @Controller
2 public class UserController {
3 @Resource(name = "user2")
4 private User user;
5
6 public void sayHi(){
7 System.out.println("hi,UserController...");
8 System.out.println(user);
9 }
10 }