通过上篇博客(也就是文章开头推荐的博客,不看上篇博客的话,只要知道Spring是什么,Spring的本质也可以)我们知道了什么是Spring,那这篇博客就带大家**用代码实际使用Spring的存和取的功能**,以及**相关注解的使用**
既然Spring是一个IOC(控制反转)容器,作为容器,那么它就具备两个最基础的功能
- 存
取
Spring容器管理的主要是对象,这些对象,我们称之为“Bean”。我们把这些对象交由Spring管理,由Spring来负责对象的创建和销毁,我们程序只需要告诉Spring,哪些需要存,以及如何从Spring中取出对象
一、Bean的存储和取用
前⾯我们提到IoC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对 象。 也就是**bean的存储**
要把某个对象交给IOC容器管理,需要添加相对应的注解
共有两种注解类型可以实现:
- 类注解:@Controller、@Service、@Repository、@Component、@Configuration
- 方法注解:@Bean
1、存 —— 五大类注解
@Controller(控制器存储)
使用@Controller存储以及相关取用
@Controller
public class UserController {
public void doUserController(){
System.out.println("do Controller...");
}
}
接下来我们学习如何从Spring容器中获取对象
此处先跟着走,不要太过理解,存对象讲完后下面就是详细讲如何取对象
@SpringBootApplication
public class SpringBoot2Application {
public static void main(String[] args) {
// 获取Spring上下文对象
ApplicationContext context = SpringApplication.run(SpringBoot2Application.class, args);
// 从context(Spring上下文)中获取bean
// 通过类型拿bean
UserController userController = context.getBean(UserController.class);
userController.doUserController();
}
ApplicationContext本质就是Spring的运行环境封装成的对象
Spring上下文的理解:
ApplicationContext 翻译过来就是: Spring 上下⽂ 因为对象都交给 Spring 管理了,所以获取对象要从 Spring 中获取,那么就得先得到 Spring 的上下文
关于上下⽂的概念
上学时, 阅读理解经常会这样问: 根据上下⽂, 说⼀下你对XX的理解 在计算机领域, 上下⽂这个概念, 咱们最早是在学习线程时了解到过, ⽐如我们应⽤进⾏线程切换的时 候,切换前都会把线程的状态信息暂时储存起来,这⾥的上下⽂就包括了当前线程的信息,等下次该 线程⼜得到CPU时间的时候, 从上下⽂中拿到线程上次运⾏的信息 **这个上下⽂, 就是指当前的运⾏环境**, **也可以看作是⼀个容器**, 容器⾥存了很多内容, 这些内容是当前运⾏的环境
观察运⾏结果, 发现成功从Spring中获取到Controller对象, 并执⾏Controller的doController⽅法
如果把@Controller删掉,再观察运行结果
报错信息显⽰: 找不到类型是: com.example.springboot2.controller.UserController的bean
其他四个类注解操作类似
- @Service(服务存储)
- @Repository(仓库存储)
- @Component(组件存储)
- @Configuration(配置存储)
UserService、UserRepository等文件就不在这里放源码了,模仿我上面的UserController源码写一下即可
@SpringBootApplication
public class SpringBoot2Application {
public static void main(String[] args) {
// 获取Spring上下文对象
ApplicationContext context = SpringApplication.run(SpringBoot2Application.class, args);
// 从context(Spring上下文)中获取bean
// 通过类型拿bean
UserController userController = context.getBean(UserController.class);
userController.doUserController();
UserService userService = context.getBean(UserService.class);
userService.doService();
UserRepository userRepository = context.getBean(UserRepository.class);
userRepository.doRepository();
UserComponent userComponent = context.getBean(UserComponent.class);
userComponent.doComponent();
UserConfig userConfig = context.getBean(UserConfig.class);
userConfig.doConfig();
}
观察运⾏结果, 发现全部取出成功
2、取 —— Spring取Bean对象(详细)
拿上面的Controller在这里作为例子讲解
@SpringBootApplication
public class SpringBoot2Application {
public static void main(String[] args) {
// 获取Spring上下文对象
ApplicationContext context = SpringApplication.run(SpringBoot2Application.class, args);
// 从context(Spring上下文)中获取bean
// 通过类型拿bean
UserController userController = context.getBean(UserController.class);
userController.doUserController();
// 通过Bean名(对象名)取对象
UserController userController2 = (UserController) context.getBean("userController");
userController2.doUserController();
// 通过Bean名和类型加在一起取对象
UserController userController3 = context.getBean("userController",UserController.class);
userController3.doUserController();
}
可以看到上面有三种方式取到这个对象
下面我们针对这段代码来个逐句解析
ApplicationContext context = SpringApplication.run(SpringBoot2Application.class, args);
这句代码的后半部分是不是特别熟悉?对,后半部分的意思就是启动我们的SpringBoot项目,每个SpringBoot项目都有后半部分
而这里我们用了ApplicationContext类型来接收它的返回值,这里ApplicationContext可以简单理解为将Spring环境封装而成的对象
我们将这一步叫做获取Spring上下文对象
然后后面三种方式,都是获取Bean对象
getBean是该对象的方法,专门用来取出对象,有三种传参方式可供选择
- 第一种方式是通过对象字节码文件来取出对象的
- 第二种方式是通过Bean名称来取出对象的
- 第三种方式是通过Bean名称和对象字节码一起取出该对象的
其中2,3种都涉及到根据名称来获取对象. bean的名称是什么呢?
Spring bean是Spring框架在运⾏时管理的对象, Spring会给管理的对象起⼀个名字.
⽐如学校管理学⽣, 会给每个学⽣分配⼀个学号, 根据学号, 就可以找到对应的学⽣.
Spring也是如此, 给每个对象起⼀个名字, 根据Bean的名称(BeanId)就可以获取到对应的对象.
3、Bean命名约定
我们可以查阅官方文档:
Bean Overview :: Spring Framework
程序开发⼈员不需要为bean指定名称(BeanId), 如果没有显式的提供名称(BeanId),Spring容器将为该 bean⽣成唯⼀的名称.(**Bean名称可以自己指定,若不指定,则由Spring生成唯一的名称**)
命名约定使⽤Java标准约定作为实例字段名. 也就是说,**bean名称以⼩写字⺟开头,然后使⽤驼峰式⼤⼩写.**
⽐如
- 类名: UserController, Bean的名称为: userController
- 类名: AccountManager, Bean的名称为: accountManager
- 类名: AccountService, Bean的名称为: accountService
也有⼀些特殊情况, 当有多个字符并且第⼀个和第⼆个字符都是⼤写时, 将保留原始的⼤⼩写. 这些规则 与java.beans.Introspector.decapitalize (Spring在这⾥使⽤的)定义的规则相同
⽐如
- 类名: UController, Bean的名称为: UController
- 类名: AManager, Bean的名称为: AManager
4、为什么需要这么多类注解?
这个也是和咱们前⾯讲的应⽤分层是呼应的. 让程序员看到类注解之后,就能直接了解当前类的⽤途
- @Controller:控制层, 接收请求, 对请求进⾏处理, 并进⾏响应.
- @Servie:业务逻辑层, 处理具体的业务逻辑.
- @Repository:数据访问层,也称为持久层. 负责数据访问操作
- @Configuration:配置层. 处理项⽬中的⼀些配置信息
- @Component:元注解,也就是说可以注解其他类注解,上面的四个注解中源码都包含@Component,当有的类用上面四个都不合适的时候,再来考虑用@Component
这和每个省/市都有⾃⼰的⻋牌号是⼀样的.
⻋牌号都是唯⼀的, 标识⼀个⻋辆的. 但是为什么还需要设置不同的⻋牌开头呢.
⽐如陕西的⻋牌号就是:陕X:XXXXXX,北京的⻋牌号:京X:XXXXXX,甚⾄⼀个省不同的县区也 是不同的,⽐如西安就是,陕A:XXXXX,咸阳:陕B:XXXXXX,宝鸡,陕C:XXXXXX,⼀样.
这样做的好处除了可以节约号码之外,更重要的作⽤是可以直观的标识⼀辆⻋的归属地.
程序的应用分层,调用流程如下:
5、类注解之间的关系
查看 @Controller / @Service / @Repository / @Configuration 等注解的源码发现:
其实这些注解⾥⾯都有⼀个注解 @Component,说明它们本⾝就是属于 @Component 的“⼦类”. @Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller , @Service , @Repository 等. 这些注解被称为 @Component 的衍⽣注解.
@Controller , @Service 和 @Repository ⽤于更具体的⽤例(分别在控制层, 业务逻辑层, 持久化层), 在开发过程中, 如果你要在业务逻辑层使⽤ @Component 或@Service,显然@Service是更 好的选择
⽐如杯⼦有喝⽔杯, 刷⽛杯等, 但是我们更倾向于在⽇常喝⽔时使⽤⽔杯, 洗漱时使⽤刷⽛杯.
6、存和取 —— 方法注解@Bean
类注解是添加到某个类上的, 但是存在两个问题:
- 1. 使⽤外部包⾥的类, 没办法添加类注解
- 2. ⼀个类, 需要多个对象, ⽐如多个数据源
存 —— @Bean
这种场景, 我们就需要使⽤⽅法注解 @Bean
@Configuration
public class BeanConfig {
@Bean
public UserInfo userInfo1(){
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setName("zhangsan");
userInfo.setAge(12);
return userInfo;
}
}
注:UserInfo只是随便定义的实体类,在此处并不重要,代码如下:
@Data
public class UserInfo {
private Integer id;
private String name;
private Integer age;
}
注:文章从此处开始往下的取对象的操作就不放前面的Spring上下文对象了,只放不重复重要的代码,若想找完整的代码往上面到Controller那里复制一下,然后把取对象那两行代码换成下面这种就行啦,非常简单而且绝对正确
取 —— 从Bean取对象:
UserInfo userInfo = (UserInfo)context.getBean("userInfo1");
System.out.println(userInfo);
有细心的同学会发现,我们这个类不是UserConfig么,为什么返回值用UserInfo?
请注意,咱这个是方法注解,是将该方法交给Spring托管,该方法的返回值是UserInfo,因此咱取出对象的返回值是UserInfo
观察运⾏结果, 发现取出成功
定义多个对象
对于同⼀个类, 如何定义多个对象呢?
⽐如多数据源的场景, 类是同⼀个, 但是配置不同, 指向不同的数据源
@Configuration
public class BeanConfig {
@Bean
public UserInfo userInfo1(){
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setName("zhangsan");
userInfo.setAge(12);
return userInfo;
}
@Bean
public UserInfo userInfo2(){
UserInfo userInfo = new UserInfo();
userInfo.setId(2);
userInfo.setName("lisi");
userInfo.setAge(13);
return userInfo;
}
}
定义多个对象的话,此时我们再根据类型获取对象,获取的是哪个对象呢?
UserInfo userInfo = context.getBean(UserInfo.class);
System.out.println(userInfo);
运行结果:
报错信息显⽰: 期望只有⼀个匹配, 结果发现了两个, UserInfo1,UserInfo2
从报错信息中, 可以看出来, @Bean 注解的bean, bean的名称就是它的⽅法名
接下来我们根据名称来获取bean对象
UserInfo userInfo = (UserInfo) context.getBean("userInfo1");
System.out.println(userInfo);
UserInfo userInfo2 = (UserInfo)context.getBean("userInfo2");
System.out.println(userInfo2);
观看运行结果,发现取出成功,输出正确
可以看到, @Bean 可以针对同⼀个类, 定义多个对象
7、重命名Bean
可以通过设置name属性给Bean对象进行重命名操作,如下图所示:
@Configuration
public class BeanConfig {
@Bean(name = "u1")
public UserInfo userInfo1(){
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setName("zhangsan");
userInfo.setAge(12);
return userInfo;
}
}
此时我们就可以通过u1来获取到UserInfo对象了,如下代码所示:
UserInfo userInfo = (UserInfo) context.getBean("u1");
System.out.println(userInfo);
8、扫描路径
- Q: 使⽤前⾯学习的四个注解声明的bean,⼀定会⽣效吗?
- A: 不⼀定(原因:bean想要⽣效,还需要被Spring扫描)
下面我们通过修改项目工程的目录结构,来测试bean对象是否生效:
再运行代码就报错
报的错误是没有找到bean对象
为什么没有找到bean对象呢?
使⽤五⼤注解声明的bean,要想⽣效,还需要配置扫描路径,让Spring扫描到这些注解,也就是通过@ComponentScan 来配置扫描路径.
他就会扫描这个目录及以下的所有文件
也可以用{}配置多个包路径
@ComponentScan({"com.example.springboot2"})
这种做法仅作了解,不推荐使用
那为什么前⾯没有配置 @ComponentScan注解也可以呢?
@ComponentScan 注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解 @SpringBootApplication 中了
默认扫描的范围是SpringBoot启动类所在包及其⼦包
在配置类上添加@ComponentScan 注解, 该注解默认会扫描该类所在的包下所有的配置类
二、DI详解
在上篇博客我们讲解了控制反转IoC的细节,接下来呢,我们学习依赖注⼊DI的细节
依赖注⼊是⼀个过程,是指IoC容器在创建Bean时, 去提供运⾏时所依赖的资源,⽽资源指的就是对象.
简单来说,就是把对象取出来放到某个类的属性中
在⼀些⽂章中, 依赖注⼊也被称之为 "对象注⼊", "属性装配", 具体含义需要结合⽂章的上下⽂来理解
在上篇博客(文章开头推荐的博客)中我们用到了IOC,如下图
图片上标注的代码用DI的方法实现的话其实就是
@Autowired
private Framework framework;
关于依赖注⼊, Spring也给我们提供了三种⽅式:
- 属性注⼊(Field Injection) -> 最常用
- 构造⽅法注⼊(Constructor Injection)
- Setter 注⼊(Setter Injection)
接下来,我们分别来看这三种方式的用法和区别
下⾯我们按照实际开发中的模式,将 Service 类注⼊到 Controller 类中
1、属性注入
属性注⼊是使⽤ @Autowired 实现的,将 Service 类注⼊到 Controller 类中。
Service 类的实现代码如下:
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void sayHi() {
System.out.println("Hi,UserService");
}
}
Controller 类的实现代码如下:
@Controller
public class UserController {
//注⼊⽅法1: 属性注⼊
@Autowired
private UserService userService;
public void sayHi(){
System.out.println("hi,UserController...");
userService.sayHi();
}
}
获取Controller中的sayHi方法
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下⽂对象
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class,args);
//从Spring上下⽂中获取对象
UserController userController = (UserController) context.getBean("userController")
//使⽤对象
userController.sayHi();
}
}
最终结果如下:
去掉@Autowired , 再运⾏⼀下程序看看结果
2、构造方法注入:
构造⽅法注⼊是在类的构造⽅法中实现注⼊,如下代码所⽰:
@Controller
public class UserController2 {
//注⼊⽅法2: 构造⽅法
private UserService userService;
@Autowired
public UserController2(UserService userService) {
this.userService = userService;
}
public void sayHi(){
System.out.println("hi,UserController2...");
userService.sayHi();
}
}
注意事项:如果类只有⼀个构造⽅法,那么 @Autowired 注解可以省略;如果类中有多个构造⽅法, 那么需要添加上 @Autowired 来明确指定到底使⽤哪个构造⽅法。
3、Setter注入
Setter 注⼊和属性的 Setter ⽅法实现类似,只不过在设置 set ⽅法的时候需要加上 @Autowired 注 解 ,如下代码所⽰:
@Controller
public class UserController3 {
//注⼊⽅法3: Setter⽅法注⼊
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void sayHi(){
System.out.println("hi,UserController3...");
userService.sayHi();
}
}
4、三种注⼊优缺点分析
实际使用基本还是属性注入(@Autowired)
1、属性注⼊
优点: 简洁,使⽤⽅便
缺点:
- 只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只有在使⽤的时候才会出现 NPE(空指针异常)
- 不能注⼊⼀个Final修饰的属性
2、构造函数注⼊(Spring 4.X推荐)
优点:
1. 可以注⼊final修饰的属性 2. 注⼊的对象不会被修改 3. 依赖对象在使⽤前⼀定会被完全初始化,因为依赖是在类的构造⽅法中执⾏的,⽽构造⽅ 法是在类加载阶段就会执⾏的⽅法。 4. 通⽤性好, 构造⽅法是JDK⽀持的, 所以更换任何框架,他都是适⽤的
缺点:
注⼊多个对象时, 代码会⽐较繁琐
3、Setter注⼊(Spring 3.X推荐)
优点: ⽅便在类实例之后, 重新对该对象进⾏配置或者注⼊
缺点:
1. 不能注⼊⼀个Final修饰的属性 2. 注⼊对象可能会被改变, 因为setter⽅法可能会被多次调⽤, 就有被修改的⻛险
更多DI相关内容参考:
Dependencies :: Spring Framework
5、@Autowired存在的问题
当同一个类型存在多个bean时,使用@Autowired会存在问题
还拿这段代码举例
@Configuration
public class BeanConfig {
@Bean
public UserInfo userInfo1(){
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setName("zhangsan");
userInfo.setAge(12);
return userInfo;
}
@Bean
public UserInfo userInfo2(){
UserInfo userInfo = new UserInfo();
userInfo.setId(2);
userInfo.setName("lisi");
userInfo.setAge(13);
return userInfo;
}
}
在UserController注入UserInfo
@Controller
public class UserController {
// 属性注入
@Autowired
private UserService userService;
@Autowired
private UserInfo userInfo;
public void doUserController(){
userService.doService();
System.out.println(userInfo);
System.out.println("do Controller...");
}
}
运行报错,报错原因,不是唯一的Bean对象
如何解决上述问题呢?Spring提供了以下⼏种解决⽅案:
- @Primary
- @Qualifier
- @Resource
使⽤@Primary注解:当存在多个相同类型的Bean注⼊时,加上@Primary注解,来确定默认的实现
@Configuration
public class BeanConfig {
@Primary
@Bean
public UserInfo userInfo1(){
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setName("zhangsan");
userInfo.setAge(12);
return userInfo;
}
@Bean
public UserInfo userInfo2(){
UserInfo userInfo = new UserInfo();
userInfo.setId(2);
userInfo.setName("lisi");
userInfo.setAge(13);
return userInfo;
}
}
使⽤@Qualifier注解:指定当前要注⼊的bean对象。 在@Qualifier的value属性中,指定注⼊的bean 的名称。
@Qualifier注解不能单独使⽤,必须配合@Autowired使⽤
@Controller
public class UserController {
// 属性注入
@Autowired
private UserService userService;
@Qualifier("userInfo2")
@Autowired
private UserInfo userInfo;
public void doUserController(){
userService.doService();
System.out.println(userInfo);
System.out.println("do Controller...");
}
}
使⽤@Resource注解:是按照bean的名称进⾏注⼊。通过name属性指定要注⼊的bean的名称
@Controller
public class UserController {
// 属性注入
@Autowired
private UserService userService;
@Resource(name = "userInfo2")
private UserInfo userInfo;
public void doUserController(){
userService.doService();
System.out.println(userInfo);
System.out.println("do Controller...");
}
}
6、⾯试题 —— @Autowird 与 @Resource的区别
@Autowired 是spring框架提供的注解,⽽@Resource是JDK提供的注解
@Autowired 默认是按照类型注⼊,⽽@Resource是按照名称注⼊
使⽤时设置的参数不同:相⽐于 @Autowired 来说,@Resource ⽀持更多的参数设置,例如 name 设置,根据名称获取 Bean。