一、IoC详解
IoC控制反转,就是将对象的控制权交给Spring的IoC容器,由IoC容器创建及管理对象;也就是bean的存储。
共有两类注解类型可以实现:1、类注解:@Controller、@Service、@Repository、@Component、@Configuration。2、方法注解:@Bean。
1、Bean的存储
(1)@Controller(控制器存储)
使用@Controller注解,存储bean的代码如下:
@Controller public class UserController { public void sayHi() { System.out.println("hi, UserController"); } }
这个对象加了@Controller注解,就把这个对象放在Spring容器中了,下面是从Spring容器获取对象,代码如下:
@SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring的上下文 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring的上下文中获取对象 UserController bean = (UserController)context.getBean(UserController.class); bean.sayHi(); } }
ApplicantContent 翻译过来就是:Spring 上下文。因为对象交给Spring管理了,所以获取对象要从Spring中获取,那么就得先得到Spring的上下文。
关于上下文的概念:在计算机领域,上下文这个概念,在线程那已经了解过了,比如应用进行线程切换的时候,切换前都会把线程的状态信息暂时存储起来,这里的上下文就包括了当前线程的信息,等下次该线程又得到CPU时间的时候,从上下文中拿到线程上次运行的信息。
这里的上下文,就是当前的运行环境,也可以看做是一个容器,容器里存了很多内容,这些内容是当前运行的环境。
观察运行结果,发现成功从Spring中获取了Controller 对象,并执行Controller的sayHi方法,如图:
如果把UserController类上面的注解@Controller去掉,就会报错,如图:
报错信息:找不到类型是com.example.springioc2.controller.UserController的bean;因为没加这个注解,自然也就没声明把这个类交给Spring管理,Spring也就找不到该类的bean了。
获取bean对象的其他方式
上述代码是根据类型来查找对象的,如果Spring容器中,同一个类型存在多个bean的话,要怎么获取?ApplicationContext也提供了其他获取bean的方式,ApplicationContext获取bean对象的功能,是父类BeanFactory提供的功能。如下代码:
public interface BeanFactory { //以上省略... // 1. 根据bean名称获取bean Object getBean(String var1) throws BeansException; // 2. 根据bean名称和类型获取bean <T> T getBean(String var1, Class<T> var2) throws BeansException; // 3. 按bean名称和构造函数参数动态创建bean,只适⽤于具有原型(prototype)作⽤域的bean Object getBean(String var1, Object... var2) throws BeansException; // 4. 根据类型获取bean <T> T getBean(Class<T> var1) throws BeansException; // 5. 按bean类型和构造函数参数动态创建bean, 只适⽤于具有原型(prototype)作⽤域的bean <T> T getBean(Class<T> var1, Object... var2) throws BeansException; //以下省略... }
最常用的是1、2、4种,这三种方式(根据bean名称获取bean、根据bean类型和名称获取bean、根据bean类型获取bean),获取到的bean是一样的。
其中1、2种都涉及到根据名称来获取对象,那么bean的名称是什么呢?
Spring bean 是Spring框架在运行时管理的对象,Spring会给管理的对象起一个名字。
比如学校管理学生,会给每个学生分配一个学号,根据学号,就可以找到对应的学生。
Spring也是如此,给每个对象都起一个名字,根据Bean的名称(BeanId)就可以获取到对应的对象。
Bean 命名约定
官方文档:Bean Overview :: Spring Framework
程序开发人员不需要为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 SpringIoC2Application { public static void main(String[] args) { //获取Spring的上下文 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring的上下文中获取对象 //根据bean类型,从Spring上下文中获取对象 UserController userController1 = (UserController)context.getBean(UserController.class); //根据bean名称,从Spring上下文获取对象 UserController userController2 = (UserController)context.getBean("userController"); //根据bean名称+类型,从Spring上下文获取对象 UserController userController3 = (UserController) context.getBean("userController", UserController.class); System.out.println(userController1); System.out.println(userController2); System.out.println(userController3); } }
运行结果如下图:
地址都是一样的,说明对象是同一个。
ApplicationContext 和 BeanFactory (常见面试题)
1、从继承关系和功能方面来说:Spring容器有两个顶级的接口:BeanFactory 和 ApplicationContext。其中BeanFactory提供了基础的访问容器的能力,而 ApplicationContext 属于 BeanFactory 的子类,它除了继承 BeanFactory 的所有功能外,它还有自己独特的特性,还添加了对国际化支持、资源访问支持、以及事件传播等方面支持。
2、从性能方面来说:ApplicationContext是一次性加载并初始化所有Bean对象(提前加载),而 BeanFactory 是需要哪个对象了,才去加载那个对象(懒加载),因此更轻量。
(2)@Service(服务存储)
使用@Service存储bean的代码如下:
@Service public class UserService { public void doService() { System.out.println("do Service..."); } }
读取bean的代码:
@SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring上下文对象 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring中获取UserService对象 UserService userService = context.getBean(UserService.class); //使用对象 userService.doService(); } }
执行结果如下:
把注解@Service删掉,会报错,如图:
和去掉@Controller注解一样的报错原因一样,找不到bean。
(3)@Repository(仓库存储)
使用@Repository存储bean代码如下:
@Repository public class UserRepository { public void doRepository() { System.out.println("do Repository..."); } }
读取bean的代码:
@SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring上下文对象 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring中获取UserRepository对象 UserRepository userRepository = context.getBean(UserRepository.class); //使用对象 userRepository.doRepository(); } }
运行结果:
同样把注解@Repository去掉,会报错,如图:
原因和上面的一样。
(4)@Component(组件存储)
使用@Component存储bean的代码:
@Component public class UserComponent { public void doComponent() { System.out.println("do Component..."); } }
读取bean的代码:
@SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring上下文对象 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring中获取UserComponent对象 UserComponent userComponent = context.getBean(UserComponent.class); //使用对象 userComponent.doComponent(); } }
执行结果:
如果把注释@Component去掉,报错和上面的一样,如图:
(5)@Configuration(配置存储)
使用@Configuration存储bean的代码如下:
@Configuration public class UserConfiguration { public void doConfiguration() { System.out.println("do Configuration..."); } }
读取bean的代码:
@SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring上下文对象 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring中获取UserComponent对象 UserConfiguration userConfiguration = context.getBean(UserConfiguration.class); //使用对象 userConfiguration.doConfiguration(); } }
执行结果如下:
同样,把注释@Configuration去掉,会报错,原因和上面的都一样,结果如下:
2、为什么要这么多类注解?
这个和前面的应用分层是对应的,程序员看到这些类注解后,就能直接了解当前类的用途。
@Controller:控制层,接收请求,对请求进行处理,并进行响应。
@Service:业务逻辑层,处理具体的业务逻辑。
@Repository:数据访问层,也称为持久层。负责数据访问操作。
@Configuration:配置层,处理项目中的一些配置信息。
和三层架构的对应关系:@Controller 对应 表现层,@Service 对应 业务逻辑层,@Repository 对应 数据层。
这里和每个省 / 市都有自己的车牌号一样,车牌号都是唯一的,我们看到车牌,就能知道车主是哪里的了 / 车的归属地,比如 粤X、京X等等。这样做的好处即可以节约号码,还有更重要的作用:标识一辆车的归属地。
程序的应用分层,调用流程如下:
类注解之间的关系:
查看@Controller、@Service 、@Repository、@Configuration注解的源码我们发现,它们里面都有一个@Component注解。
这也能说明它们本身就是属于 @Component 的 “子类”。而@Component是一个元注解,也就是说可以注解其他类的注解,如 @Controller、@Service、@Repository 等等,这些注解则被称为 @Component的衍生注解。
@Controller、@Service 和 @Repository 用于更具体的用例(分别为控制层、业务逻辑层、数据访问层),在开发过程中,如果你要在业务逻辑层使用@Component或者@Service,显然@Service是更好的选择。
好比是杯子有喝水的(水杯),也有刷牙的(刷牙杯)等等,但我们更倾向于在日常喝水时使用水杯,洗漱时使用刷牙杯。
更多资料参考:Classpath Scanning and Managed Components :: Spring Framework
3、方法注解 @Bean
类注解是添加到某个类上的,但是存在两个问题:1、使用外部包里的类,没办法添加类注解。2、一个类,需要多个对象,比如多个数据源。这些场景,我们就需要使用方法注解:@Bean
以下是方法注解@Bean使用的代码:
public class BeanConfig { @Bean public UserInfo userInfo() { UserInfo user = new UserInfo(); user.setId(6); user.setName("zhangsan"); user.setAge(18); return user; } }
读取bean的代码:
@SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring上下文对象 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring中获取对象 UserInfo userInfo = context.getBean(UserInfo.class); //使用对象 System.out.println(userInfo); } }
但我们发现,获取不到,程序报错了,如图:
原因就是方法注解要搭配类注解使用,上面却没有搭配。
为什么要搭配类注解呢?因为Spring Boot项目会引入非常多的依赖,里面的代码的方法也会有很多注解,如果都进行扫描,那就太耗时了,不如搭配类注解,Spring就知道从哪些类中扫描,大大的提高了性能。
(1)方法注解要配合类注解使用
在Spring框架的设计中,方法注解 @Bean 要配合类注解才能将对象正常的存储到Spring容器中,代码如下:
@Component public class BeanConfig { @Bean public UserInfo userInfo() { UserInfo user = new UserInfo(); user.setId(6); user.setName("zhangsan"); user.setAge(18); return user; } }
执行结果如下:
(2)定义多个对象
对于同一个类,定义多个对象(多个方法使用@Bean注解);比如多数据源的场景,类是同一个,但配置是不同的,指向不同的数据源。
@Component public class BeanConfig { @Bean public UserInfo userInfo1() { UserInfo user = new UserInfo(); user.setId(6); user.setName("zhangsan"); user.setAge(18); return user; } @Bean public UserInfo userInfo2() { UserInfo user = new UserInfo(); user.setId(7); user.setName("lisi"); user.setAge(19); return user; } }
定义了多个对象的话,我们根据类型获取对象,获取的是哪个对象呢?代码如下:
@SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring上下文对象 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring中获取对象 UserInfo userInfo = context.getBean(UserInfo.class); //使用对象 System.out.println(userInfo); } }
执行结果,可以看到,报错了:
可以看到,报错信息显示:期望只有一个匹配,结果发现两个:userInfo1,userInfo2。从报错信息中,可以看出来,@Bean注解的bean,bean的名称就是它的方法名。
我们现在改一下代码,根据名称来获取bean对象,代码如下:
@SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring上下文对象 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring中获取对象 UserInfo userInfo1 = (UserInfo) context.getBean("userInfo1"); UserInfo userInfo2 = (UserInfo) context.getBean("userInfo2"); //使用对象 System.out.println(userInfo1); System.out.println(userInfo2); } }
运行结果如下:
可以看到,@Bean可以针对同一个类,定义多个对象。
(3)重命名 Bean
可以通过设置 name 属性,给Bean对象进行重命名操作,代码如下:
@Component public class BeanConfig { @Bean(name = {"u1", "userInfo1"}) public UserInfo userInfo1() { UserInfo user = new UserInfo(); user.setId(6); user.setName("zhangsan"); user.setAge(18); return user; } }
此时使用 u1 就可以获取到User对象了,代码如下:
@SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring上下文对象 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring上下文中获取对象 UserInfo u1 = (UserInfo)context.getBean("u1"); //使用对象 System.out.println(u1); } }
运行结果如下:
使用 useInfo1 也可以获取到User对象,两个字符串都是对UserInfo的重命名,代码如下:
@SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring上下文对象 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring上下文中获取对象 UserInfo u1 = (UserInfo)context.getBean("userInfo1"); //使用对象 System.out.println(u1); } }
执行结果:
其中,@Bean中的name也可以省略,代码如下:
@Component public class BeanConfig { @Bean({"u1", "userInfo1"}) public UserInfo userInfo1() { UserInfo user = new UserInfo(); user.setId(6); user.setName("zhangsan"); user.setAge(18); return user; } }
只有一个名称时,{ } 也可以省略,代码如下:
@Component public class BeanConfig { @Bean("u1") public UserInfo userInfo1() { UserInfo user = new UserInfo(); user.setId(6); user.setName("zhangsan"); user.setAge(18); return user; } }
4、扫描路径
现在有个问题,就是不管啥类,只要搭配了五大注解或者五大注解+Bean,Spring就能启动成功吗?
其实不然,原因就是扫描路径的问题,Spring默认只会对启动类所在的目录下进行扫描,并且扫描的是搭配了五大注解的类或者搭配五大注解+Bean的类。
启动类:加了 @SpringBootApplication 注解的类
现在测试一下,把启动类移动一下,此时启动类所在路径如图:
试试看还能不能获取到UserService对象,UserService类的代码如下:
@Service public class UserService { public void doService() { System.out.println("do Service..."); } }
启动柜类代码如下:
@SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring上下文对象 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring中获取UserService对象 UserService userService = context.getBean(UserService.class); //使用对象 userService.doService(); } }
执行结果如下图:
报错解释:没有bean的类型是UserService的。
这里为什么没有找到bean对象呢?原因:即使使用了五大注解,或者五大注解 + @Bean注解,要想生效,还需要配置扫描路径,让Spring扫描到这些注解下的类,也就是通过 @ComponentScan 来配置。加了 @ComponentScan注解 的代码如下:
@ComponentScan({"com.example.springioc2.service"}) @SpringBootApplication public class SpringIoC2Application { public static void main(String[] args) { //获取Spring上下文对象 ApplicationContext context = SpringApplication.run(SpringIoC2Application.class, args); //从Spring中获取UserService对象 UserService userService = context.getBean(UserService.class); //使用对象 userService.doService(); } }
其中注解@ComponentScan的括号里面,加的是UserService类的包路径,直接复制即可,在Service类里面,如下图:
执行结果如下:
可以拿到bean对象了。
其中注解@ComponentScan括号里的 { } ,可以加多个大括号,表示扫描多个路径,如图:
那为什么之前没有配置 @ComponentScan注解 ,也可以正常启动?原因是: @ComponentScan注解 虽然没有显示配置,但是实际上已经包含在启动类声明注解@SpringBootApplication 中了。
默认扫描的范围是:SpringBoot启动类所在包及其子包;在配置类添加 @ComponentScan注解,该注解默认会扫描该类所在的包下所有的配置类,如图:
开发时的推荐做法:把启动类放在我们希望扫描的包的路径下,这样我们自己写的代码就都可以被扫描到了,如图: