Spring IoCDI_2_使用篇
通过前一篇文章,我们已经知道了Spring IoC和DI的基本概念,这篇文章我们将系统地学习Spring-IoC和DI的操作。
前面我们提到的IoC控制反转,就是将对象的控制权交给Spring的IoC容器,由IoC容器创建及管理对象。也就是bean(Spring管理的对象统称为“bean”)的存储。
IoC详解
Bean的存储
将对象存储在Spring中共有两种注解类型可以实现:
1、类注解:@Controller、@Service、@Repository、@Component、@Configuration
2、方法注解:@Bean
@Controller(控制器存储)
如图,在启动类中创建一个子目录Controller:
类中代码如下:
@Controller
public class HelloController {public void sayHi(){System.out.println("hello Controller……");}
}
如何知道当前对象已经存在Spring容器中了呢?
接下来我们学习如何从Spring容器中获取对象。
点进Spring的启动类中,添加上如下代码:
@SpringBootApplication
public class SpringIoCApplication {public static void main(String[] args) {//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);//从Spring上下文中获取对象HelloController bean = run.getBean(HelloController.class);//使用对象bean.sayHi();}}
观察运行结果,发现成功获取到了Controller对象,并执行了sayHi()方法:把@Controller删去重新执行一遍代码:
将错误信息翻译一下:Spring容器中没有找到这个Bean。
获取Bean的其他方式
上述代码是根据类型来查找对象,如果Spring容器中,同一类型存在多个bean的话,怎么来获取呢?
ApplicationContext也提供了其他获取bean的方式,ApplicationContext获取bean对象的功能,是父类BeanFactory提供的功能。
观察getBean方法的源码:
常用的是上述1,2,4种,这三种方式,获取到的bean是一样的(单例模式)。
其中1,2种都涉及到根据名称来获取对象。bean的名称是什么呢?
刚才我们讲了bean是spring框架在运行时管理的对象,Spring会给管理的对象起一个名字,就是bean的名称,bean的名称在Spring中是唯一的,类似于学生的学号或者人的身份证号。
Bean的命名约定
默认情况下,首字母小写的驼峰表示(如:我们刚才写的HelloController,Bean的名称为:helloController)。特殊情况下,如果前两个字母均为大写,Bean的名称就是原来的类名(如:UController,Bean的名称是UController)。
根据这个命名规则,我们来获取Bean:
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);//根据Bean的名称获取BeanHelloController helloController = (HelloController) run.getBean("helloController");//根据Bean的名称和类型获取BeanHelloController bean = run.getBean("helloController", HelloController.class);//根据Bean的类型获取BeanHelloController bean1 = run.getBean(HelloController.class);System.out.println(helloController);System.out.println(bean1);System.out.println(bean);
运行结果:
地址一样,说明对象是同一个。
常见面试题:ApplicationContext VS BeanFactory
1、关系上,他们是父子关系,BeanFactory的功能ApplicationContext都有。除此之外,ApplicationContext还具备环境、资源管理等功能。
2、性能上
Beanfactory是懒加载(什么时候使用什么时候加载),ApplicationContext是提前加载(一开始就把全部对象加载完了)。
@Service(服务存储)
Service类代码:
@Service
public class UserService {public void sayHi(){System.out.println("hello service");}
}
获取Bean的代码
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);UserService bean = run.getBean(UserService.class);bean.sayHi();
运行结果:
@Repository(仓库存储)
Repository类:
@Repository
public class UserRepository {public void sayHi(){System.out.println("hello Repository");}
}
获取Bean的代码:
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);UserRepository bean = run.getBean(UserRepository.class);bean.sayHi();
运行结果:
@Component(组件存储)
Component类:
@Component
public class UserComponent {public void sayHi(){System.out.println("hello Component");}
}
获取Bean:
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);UserComponent bean = run.getBean(UserComponent.class);bean.sayHi();
运行结果:
@Configuration(配置存储)
Configuration类:
@Configuration
public class UserConfig {public void sayHi(){System.out.println("hello configuration");}
}
获取Bean :
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);UserConfig bean = run.getBean(UserConfig.class);bean.sayHi();
运行结果:
为什么要有那么多的类注解?
这与我们前面提到的应用分层是呼应的。让程序员看到类注解之后,就能直接了解当前类的用途。
@Controller:控制层,接收请求,对请求进行处理,并进行响应。
@Service:业务逻辑层,处理具体的业务逻辑。
@Reposity:数据访问层,也称为持久层,负责数据访问操作。
@Configuration:配置层,处理项目中的一些配置信息。
调用流程如下:
查看这些注解的源码: 可以发现它们里面都有一个相同的@Component(元注解)注解,说明它们都是@Component的“子类”。
“@Controller”,@Service和@Repository用于更具体的用例(分别在控制层,业务逻辑层,持久化层)。
方法注解@Bean
类注解是添加到某个类上的,但是存在两个问题:
1、使用外部包里的类,没办法添加类注解。
2、一个类需要多个对象比如多个数据源。
这种情况我们就需要使用方法注解@Bean。
我们先来看看方法注解如何使用:
先创建一个外部包类UserInfo:
@Data
public class UserInfo {private String name;private Integer age;private Integer id;public UserInfo(){}public UserInfo(String name) {this.name = name;}
}
使用@Bean注解:
public class BeanConfig {@Beanpublic UserInfo userInfo(){return new UserInfo("zhangsan");}
}
获取Bean:
ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);UserInfo bean = run.getBean(UserInfo.class);System.out.println(bean)
将代码写入后发现执行结果如下 :
找不到Bean,这是为什么呢?
在Spring框架的涉及中,方法注解@Bean要配合类注解才能将对象正常存储到Spring容器中。
修改代码如下:
@Configuration
public class BeanConfig {@Beanpublic UserInfo userInfo(){return new UserInfo("zhangsan");}
}
定义多个对象
对于同一个类如何定义多个对象呢?
修改@Bean类如下:
@Configuration
public class BeanConfig {@Beanpublic UserInfo userInfo(){return new UserInfo("zhangsan");}@Beanpublic UserInfo userInfo1(){return new UserInfo("lisi");}@Beanpublic UserInfo userInfo2(){return new UserInfo("wangwu");}
}
如果我们此时通过类型获取对象,获取的是哪个对象呢?
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);UserInfo bean = run.getBean(UserInfo.class);System.out.println(bean);
运行结果:报错信息显示:期望只有一个匹配,但是根据这个类型找到了3个Bean,userInfo,userInfo1,userInfo2。
接下来我们可以通过Bean的名称来获取Bean,我们刚才讲了类注解的Bean的名称,那方法注解Bean的名称又是什么呢?
其实就是——方法名。
代码如下:
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);UserInfo bean = (UserInfo) run.getBean("userInfo");System.out.println(bean);UserInfo bean1 = (UserInfo) run.getBean("userInfo1");System.out.println(bean1);UserInfo bean2 = (UserInfo) run.getBean("userInfo2");System.out.println(bean2);
运行结果:
Bean的名称总结
1、五大注解:
类名:首字母小写
如果类名前两位为大写,bean的名称为类名本身
2、@Bean
bean 名称:方法名
扫描路径
使用前面学习的注解声明的bean,一定会生效吗?
不一定(原因:bean想要生效,还需要被Spring扫描)
将启动类放到Controller目录下: 写上如下代码:
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);HelloController bean1 = run.getBean(HelloController.class);bean1.sayHi();UserService bean = run.getBean(UserService.class);bean.sayHi();
运行结果:可以看到HelloController的bean对象能够正常获取并使用,而UserService的bean对象却显示没有找到。
这是为什么呢?
Spring的默认扫描路径是当前目录和当前目录下的子目录。我们可以通过@ComponentScan来配置扫描路径。
修改代码如下:
@ComponentScan("com.example.springioc")
@SpringBootApplication
public class SpringIoCApplication {public static void main(String[] args) {//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);HelloController bean1 = run.getBean(HelloController.class);bean1.sayHi();UserService bean = run.getBean(UserService.class);bean.sayHi();}
}
运行结果:
推荐做法:
把启动类放在我们希望扫描的包的路径下,这样我们定义的bean就都可以被扫描到。
DI详解
上面我们讲解了控制反转IoC的细节,接下来,我们学习依赖注入DI的细节。
依赖注入是一个过程,是指IoC容器在创建Bean时,去提供运行时所依赖的资源,而资源指的就是对象。
关于依赖注入,Spring也给我们提供了三种方式:
1、属性注入
2、构造方法注入
3、Setter注入
属性注入
属性注入需要使用@Autowired这个注解。
例如:在HelloController类中注入UserSeverice对象。
HelloController类代码如下:
@Controller
public class HelloController {@Autowiredprivate UserService userService;public void sayHi(){userService.sayHi();System.out.println("hello Controller……");}
}
获取Bean并调用sayHi方法:
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);HelloController bean1 = run.getBean(HelloController.class);bean1.sayHi()
执行结果:
去掉 @Autowired,重新执行:
出现空指针异常。
构造方法注入
HelloController代码:
@Controller
public class HelloController {private UserService userService;public HelloController(UserService userService) {this.userService = userService;}public void sayHi(){userService.sayHi();System.out.println("hello Controller……");}
}
获取使用Bean代码 :
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);HelloController bean1 = run.getBean(HelloController.class);bean1.sayHi()
执行结果: 那如果我给HelloController再加上一个无参构造方法呢?
代码修改如下:
@Controller
public class HelloController {private UserService userService;public HelloController() {}public HelloController(UserService userService) {this.userService = userService;}public void sayHi(){userService.sayHi();System.out.println("hello Controller……");}
}
可以看到报了一个空指针异常,这是因为Spring会默认使用无参的构造方法,这就导致了userService是一个空对象 。
解决办法:
为构造方法添加@Autowired注解:
@Controller
public class HelloController {private UserService userService;public HelloController() {}
@Autowiredpublic HelloController(UserService userService) {this.userService = userService;}public void sayHi(){userService.sayHi();System.out.println("hello Controller……");}
}
那么如果我注入两个对象,并且加入带一个参数的构造方法和带两个参数的构造方法(不加注解的情况)呢?
代码如下:
@Controller
public class HelloController {private UserService userService;private UserRepository userRepository;public HelloController(UserService userService) {this.userService = userService;}public HelloController(UserService userService, UserRepository userRepository) {this.userService = userService;this.userRepository = userRepository;}public void sayHi(){userRepository.sayHi();userService.sayHi();System.out.println("hello Controller……");}
}
获取使用Bean:
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);HelloController bean1 = run.getBean(HelloController.class);bean1.sayHi()
执行结果:
翻译一下这两个错误日志:构造方法有两个,我不知道应该使用哪个构造方法(没有默认构造方法)。
我们为两个参数的构造方法添加上@Autowired注解:
@Controller
public class HelloController {private UserService userService;private UserRepository userRepository;public HelloController(UserService userService) {this.userService = userService;}@Autowiredpublic HelloController(UserService userService, UserRepository userRepository) {this.userService = userService;this.userRepository = userRepository;}public void sayHi(){userRepository.sayHi();userService.sayHi();System.out.println("hello Controller……");}
}
代码成功执行:
总结:
构造函数注入
1、只有一个构造函数的情况,不需要加@Autowired
2、如果有多个构造函数,需要指定默认的构造函数(通过@Autowired指定,如果未指定,默认使用无参的构造函数
构造函数规范
1、如果添加构造函数,把无参构造函数显示添加
依赖注入:
添加构造函数时,使用@Autowired告知Spring要使用的构造函数。
Setter方法注入
Setter方法注入和属性注入一样需要添加@Autowired注解,否则会报空指针异常
@Controller
public class HelloController {private UserService userService;@Autowiredpublic void setUserService(UserService userService) {this.userService = userService;}public void sayHi(){userService.sayHi();System.out.println("hello Controller……");}
}
获取使用Bean:
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);HelloController bean1 = run.getBean(HelloController.class);bean1.sayHi()
执行结果:
三种注入方式优缺点分析(八股文):
@Autowired存在问题
当同一类型存在多个bean时,使用@Autowired会存在问题。
刚才的Beanconfig类中就存在多个bean:
@Configuration
public class BeanConfig {@Beanpublic UserInfo userInfo(){return new UserInfo("zhangsan");}@Beanpublic UserInfo userInfo1(){return new UserInfo("lisi");}@Beanpublic UserInfo userInfo2(){return new UserInfo("wangwu");}
}
同样将它注入HelloController类中:
@Controller
public class HelloController {@Autowiredprivate UserInfo userInfo;public void sayHi(){System.out.println(userInfo);System.out.println("hello Controller……");}
}
获取Bean:
//获取Spring上下文对象ConfigurableApplicationContext run = SpringApplication.run(SpringIoCApplication.class, args);HelloController bean1 = run.getBean(HelloController.class);bean1.sayHi()
可以看到,此时是能正常运行的: 但是倘若我给要注入的对象换个名字:
@Controller
public class HelloController {@Autowiredprivate UserInfo user;public void sayHi(){System.out.println(user);System.out.println("hello Controller……");}
}
执行结果:
报错原因:非唯一的Bean对象。
如何解决上述问题呢?Spring提供了三种解决方案:
- @Primary
- @Qualifier
- @Resource
使用@Primary注解:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。
代码如下:
@Configuration
public class BeanConfig {@Primary//指定该Bean未默认Bean的实现@Beanpublic UserInfo userInfo(){return new UserInfo("zhangsan");}@Beanpublic UserInfo userInfo1(){return new UserInfo("lisi");}@Beanpublic UserInfo userInfo2(){return new UserInfo("wangwu");}
}
重新执行 :
使用@Qualifier注解:指定当前要注入的bean对象。在@Qualifier的value属性中,指定注入的bean的名称。(@Qualifiler注解不能单独使用,必须配合@Autowired使用)。
HelloController代码如下:
@Controller
public class HelloController {@Qualifier("userInfo1")@Autowiredprivate UserInfo user;public void sayHi(){System.out.println(user);System.out.println("hello Controller……");}
}
执行结果:
使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。
代码如下:
@Controller
public class HelloController {@Resource(name = "userInfo2")private UserInfo user;public void sayHi(){System.out.println(user);System.out.println("hello Controller……");}
}
结果:
常见面试题:@Autowired VS @Resource的区别
@Autowired是spring框架提供的注释,而@Resource是JDK提供的注释。
@Autowired默认按照类型注入,而@Resource是按照名称注入,相比于@Autowired来说,@Resource支持更多参数设置,例如name设置,根据名称获取Bean