详解IoC和DI
目录
Bean的存储
类注解
类注解的使用
关于ApplicationContext
关于getBean()
Bean的命名规则
方法注解@Bean
方法注解的使用1
方法注解的使用2
@Bean重命名
扫描路径
DI
属性注入
构造方法注入
只有一个有参构造函数
有多个构造函数
Setter注入
关于@Autowired匹配规则
方案一:使用@Primary
方案二:使用@Qualifier
方案三:使用@Resource
小结
@Autowired和@Resource的区别
依赖注入总结
IoC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象,也就是bean的存储.
Bean的存储
Spring框架为了更好的服务web应⽤程序, 提供了更丰富的注解.
共有两类注解类型可以实现:
1. 类注解:@Controller、@Service、@Repository、@Component、@Configuration.
2. ⽅法注解:@Bean.
类注解
类注解的使用
@Controller // 将对象存储到 Spring 中public class UserController { public void sayHi(){ System.out.println(\"hi, user controller...\"); }}@Servicepublic class UserService { public void sayHi(String name) { System.out.println(\"do service...); }}@Repositorypublic class UserRepo { public void sayHi() { System.out.println(\"do repo\"); }}@Componentpublic class UserComponent { public void sayHi() { System.out.println(\"do component...\"); }}@Configurationpublic class UserConfig { public void sayHi() { System.out.println(\"do config...\"); }}
@SpringBootApplicationpublic class SpringbootDemo2Application { public static void main(String[] args) { ApplicationContext context=SpringApplication.run(SpringbootDemo2Application.class, args); HelloController bean = context.getBean(HelloController.class); bean.sayHi(); UserService bean1 = context.getBean(UserService.class); bean1.doService(); UserRepo bean2 = context.getBean(UserRepo.class); bean2.doRepo(); UserComponent bean3 = context.getBean(UserComponent.class); bean3.doComponent(); UserConfig bean4 = context.getBean(UserConfig.class); bean4.doConfig(); }}
运行结果:
为什么有这么多种注解?
1.功能细分:不同的注解用于不同的目的和场景。例如,@Component、@Service、@Repository和@Controller虽然都可以用来标注类为Spring容器管理的Bean,但它们各自代表了不同的层(组件层、服务层、数据访问层和表现层)。这种细分有助于在大型项目中保持代码的组织性和可维护性。
2.语义清晰:使用特定于层的注解(如@Service、@Repository)可以提供更清晰的语义,使得其他开发者(或未来的你)在查看代码时能够更快地理解类的用途和职责。
3.自动检测与特殊功能:某些注解除了将类标记为Bean外,还提供了额外的功能。例如,@Repository注解不仅标记了持久层组件,还允许Spring为这些组件抛出的数据访问异常自动添加声明式事务管理。
4.灵活性:虽然大多数时候可以使用@Component作为通用注解来标记任何Bean,但Spring提供了更具体的注解来支持更精细的控制和配置。例如,@Scope注解用于指定Bean的作用域,@Lazy注解用于延迟Bean的初始化等。
5.扩展性和兼容性:随着Spring框架的发展,新的注解被不断引入以支持新的功能和改进。这种设计允许Spring在不破坏现有注解和代码的情况下进行扩展。
减少配置复杂性:虽然可以通过XML配置文件来实现相同的功能,但使用注解可以大大减少配置文件的复杂性,并使得配置更加接近于代码本身,便于理解和维护。
Spring IoC中提供多种注解而非单一注解,是为了满足不同的使用场景、提供清晰的语义、支持额外的功能、提高灵活性和可扩展性,并减少配置的复杂性。这种设计使得Spring成为了一个强大而灵活的框架,能够轻松地应对各种复杂的应用场景。
关于ApplicationContext
ApplicationContext 翻译过来就是: Spring 上下⽂。
因为对象都交给 Spring 管理了,所以获取对象要从 Spring 中获取,那么就得先得到 Spring 的上下⽂。
关于getBean()
下面将主要介绍其中三种getBean():
// 1. 根据 bean 名称获取 bean Object getBean (String var1) throws BeansException; // 2. 根据 bean 名称和类型获取 bean T getBean (String var1, Class var2) throws BeansException; // 3. 按 bean 名称和构造函数参数动态创建 bean, 只适⽤于具有原型 (prototype) 作⽤域的 bean Object getBean (String var1, Object... var2) throws BeansException; // 4. 根据类型获取 bean T getBean (Class var1) throws BeansException; // 5. 按 bean 类型和构造函数参数动态创建 bean, 只适⽤于具有原型 (prototype) 作⽤域的 bean T getBean (Class var1, Object... var2) throws BeansException;
前面已经使用类型获取过bean了,下面使用“名称”获取bean:
运行之后发现报错了:
当我们把名称的第一个字母改成小写字母后
运行没有问题
那么这到底是哪里出问题了呢?
我们不妨去官网查看一下bean的命名规则:
Bean的命名规则
意思大致为:正常情况下是把类型的第一个字母改为小写后作为名称;
另外,当类型的前两个字母都是大写字母时,名称就是原始的类型名;
且这样的规则和java.beans.Introspector.decapitalize的规则相同。
下面我们看看java.beans.Introspector.decapitalize的源码:
我们阅读后会发现意思和bean的命名规则相同。
使用 Introspector.decapitalize()
方法注解@Bean
类注解是添加到某个类上的, 但是存在两个问题:
1. 使⽤外部包⾥的类, 没办法添加类注解
2. ⼀个类, 需要多个对象, ⽐如多个数据源
这种场景, 我们就需要使⽤⽅法注解 @Bean
方法注解的使用1
public class BeanConfig { @Bean public UserInfo userInfo(){ return new UserInfo(\"zhangsan\"); }}@Datapublic class UserInfo { private Integer id; private String name; private Integer age; public UserInfo() { } public UserInfo(String name) { this.name = name; }}
运行出错,无法启动
但当我们在类BeanConfig上添加类注解后
@Componentpublic class BeanConfig { @Bean public UserInfo userInfo(){ return new UserInfo(\"zhangsan\"); }}@Datapublic class UserInfo { private Integer id; private String name; private Integer age; public UserInfo() { } public UserInfo(String name) { this.name = name; }}
运行结果:
因此我们知道,方法注解@Bean必须配合类注解一起使用才可以。
方法注解的使用2
当一个类含有多个方法注解时:
@Componentpublic class BeanConfig { // @Primary @Bean public UserInfo userInfo(){ return new UserInfo(\"zhangsan\"); } @Bean public UserInfo userInfo1(){ return new UserInfo(\"lisi\"); } @Bean public UserInfo UCInfo(){ return new UserInfo(\"lisi222\"); }}@Datapublic class UserInfo { private Integer id; private String name; private Integer age; public UserInfo() { } public UserInfo(String name) { this.name = name; }}
示例1
示例2
示例3
通过上面的3个例子,我们不难知道,方法注解是根据“方法名”匹配的。
@Bean重命名
@Componentpublic class BeanConfig { @Bean(name={\"u\",\"userInfo\"}) public UserInfo userInfo(){ return new UserInfo(\"zhangsan\"); } @Bean public UserInfo userInfo1(){ return new UserInfo(\"lisi\"); } @Bean public UserInfo UCInfo(){ return new UserInfo(\"lisi222\"); }}
运行结果:
name=可以省略,如下代码所示:
@Componentpublic class BeanConfig { @Bean({\"u\",\"userInfo\"}) public UserInfo userInfo(){ return new UserInfo(\"zhangsan\"); } @Bean public UserInfo userInfo1(){ return new UserInfo(\"lisi\"); } @Bean public UserInfo UCInfo(){ return new UserInfo(\"lisi222\"); }}
扫描路径
能够正常运行
但是当改变SpringbootDemo2Application的路径后
运行出错了,项目启动失败
为什么之前没有配置@ComponentScan注解也可以呢?
@ComponentScan注解虽然没有显示配置,但是实际上已经包含在启动类的声明注解@SpringBootApplication中了。
程序被Spring管理须具备以下条件:
1.被Spring扫描到(默认扫描范围是SpringBoot启动类所在包及其子包)
2.需要结合五大类注解和方法注解@Bean使用。
DI
依赖注⼊是⼀个过程,是指IoC容器在创建Bean时, 去提供运⾏时所依赖的资源,⽽资源指的就是对象.
我们使⽤ @Autowired 这个注解,完成依赖注⼊的操作。
简单来说, 就是把对象取出来放到某个类的属性中 。
关于依赖注⼊, Spring也给我们提供了三种⽅式:
1. 属性注⼊(Field Injection)
2. 构造⽅法注⼊(Constructor Injection)
3. Setter 注⼊(Setter Injection)
属性注入
@Controllerpublic class HelloController { @Autowired private UserService userService; public void sayHi(){ userService.doService(); System.out.println(\"hi, helloController...\"); }}@Servicepublic class UserService { public void doService(){ System.out.println(\"do service...\"); }}
运行结果:
构造方法注入
只有一个有参构造函数
@Controllerpublic class HelloController { private UserService userService; public HelloController(UserService userService) { this.userService = userService; } public void sayHi(){ userService.doService(); System.out.println(\"hi, helloController...\"); }}@Servicepublic class UserService { public void doService(){ System.out.println(\"do service...\"); }}
运行结果:
有多个构造函数
@Controllerpublic class HelloController { private UserService userService; public HelloController() { } public HelloController(UserService userService) { this.userService = userService; } public void sayHi(){ userService.doService(); System.out.println(\"hi, helloController...\"); }}@Servicepublic class UserService { public void doService(){ System.out.println(\"do service...\"); }}
运行结果出错了,空指针异常
为有参构造函数增加@Autowired注解后
@Controllerpublic class HelloController { private UserService userService; public HelloController() { } @Autowired public HelloController(UserService userService) { this.userService = userService; } public void sayHi(){ userService.doService(); System.out.println(\"hi, helloController...\"); }}@Servicepublic class UserService { public void doService(){ System.out.println(\"do service...\"); }}
运行结果:
关于构造函数注入
1.只有一个构造函数时,不需要加@Autowired
2.如果有多个构造函数,需要通过@Autowired指定默认的构造函数,如果未指定,默认使用无参的构造函数。
构造函数的规范:如果添加构造函数,应该把无参构造函数加上,当涉及依赖注入时,添加构造函数时使用@Autowired注解。
Setter注入
@Controllerpublic class HelloController { private UserService userService; //Setter方法注入 public void setUserService(UserService userService) { this.userService = userService; } public void sayHi(){ userService.doService(); System.out.println(\"hi, helloController...\"); }}@Servicepublic class UserService { public void doService(){ System.out.println(\"do service...\"); }}
运行出错,空指针异常:
给Setter方法增加@Autowired注解后
@Controllerpublic class HelloController { private UserService userService; //Setter方法注入 @Autowired public void setUserService(UserService userService) { this.userService = userService; } public void sayHi(){ userService.doService(); System.out.println(\"hi, helloController...\"); }}@Servicepublic class UserService { public void doService(){ System.out.println(\"do service...\"); }}
运行结果:
为什么需要给Setter方法增加@Autowired注解,因为Setter方法默认是不会被执行的。
关于@Autowired匹配规则
@Controllerpublic class UserController { @Autowired private UserInfo userInfo; public void sayHi(){ System.out.println(userInfo); System.out.println(\"hi, user controller...\"); }}@Componentpublic class BeanConfig { // @Primary @Bean public UserInfo userInfo(){ return new UserInfo(\"zhangsan\"); } @Bean public UserInfo userInfo1(){ return new UserInfo(\"lisi\"); } @Bean public UserInfo UCInfo(){ return new UserInfo(\"lisi222\"); }}@Datapublic class UserInfo { private Integer id; private String name; private Integer age; public UserInfo() { } public UserInfo(String name) { this.name = name; }}
运行结果:
但是修改UserController中的@Autowired注解的内容后,即
@Controllerpublic class UserController { @Autowired private UserInfo user; public void sayHi(){ System.out.println(user); System.out.println(\"hi, user controller...\"); }}
运行出错,项目启动失败,并且由此可以知道@Autowired是在项目启动之前注入依赖的
上面提到了两种解决方案,下面将在此基础上,再讲解另一种方案:
方案一:使用@Primary
@Componentpublic class BeanConfig { @Primary @Bean public UserInfo userInfo(){ return new UserInfo(\"zhangsan\"); } @Bean public UserInfo userInfo1(){ return new UserInfo(\"lisi\"); } @Bean public UserInfo UCInfo(){ return new UserInfo(\"lisi222\"); }}
运行结果:
方案二:使用@Qualifier
@Controllerpublic class UserController { @Qualifier(\"userInfo1\") @Autowired private UserInfo user; public void sayHi(){ System.out.println(user); System.out.println(\"hi, user controller...\"); }}
运行结果:
方案三:使用@Resource
@Controllerpublic class UserController { @Resource(name = \"UCInfo\") private UserInfo user; public void sayHi(){ System.out.println(user); System.out.println(\"hi, user controller...\"); }}
运行结果:
小结
@Autowired默认根据名称和类型去查,如果没找到,则抛异常;
如果根据名称查不到,就根据类型去查,如果匹配到多个类型,且没有配置@Qualifier,就会报错。
@Autowired和@Resource的区别
1. 来源
@Autowired:是 Spring 特有的注解,用于自动装配 bean。它源自 Spring 框架,并随着 Spring 的发展而演进。
@Resource:是 Java EE 的一部分,来自 JSR-250 规范(Common Annotations for the Java Platform),后来被 Spring 框架所支持,用于实现依赖注入。
2. 自动装配方式
@Autowired:默认情况下,@Autowired 是按类型(byType)进行自动装配的。如果 Spring 容器中恰好有一个 bean 的类型与需要注入的字段的类型相匹配,那么这个 bean 就会被注入。如果容器中存在多个相同类型的 bean,Spring 不知道要注入哪一个,这时就需要通过 @Qualifier 注解来指定要注入的 bean 的名称(ID)。此外,@Autowired 还支持按名称(byName)装配,但这需要设置 @Autowired 的 required 属性为 false,并且该字段在 Spring 容器中存在同名的 bean。但从 Spring 4.3 开始,@Autowired 也可以默认支持按名称装配(如果 Spring 容器中只有一个 bean 的名称与需要注入的字段名相同)。
@Resource:@Resource 默认情况下按名称(byName)进行装配,它会查找与字段名相同的 bean 名称。如果找不到,再按类型(byType)装配。此外,@Resource 注解允许你通过 name 和 type 属性显式指定要注入的 bean 的名称或类型。
3. 使用场景
@Autowired:更适用于在 Spring 应用内部进行自动装配,特别是当你依赖的 bean 是由 Spring 管理的,并且你希望根据类型或名称(在 Spring 4.3 及以上版本)来自动装配这些 bean 时。
@Resource:由于它是 Java EE 的一部分,因此它可以在任何支持 Java EE 的环境中使用,包括 Spring。它提供了更灵活的装配方式,允许你显式地指定要注入的 bean 的名称或类型。这在某些情况下非常有用,比如当你需要在多个不同环境中重用同一个类,但希望注入不同的 bean 时。
4. 依赖关系
@Autowired:完全依赖于 Spring 框架。
@Resource:不依赖于 Spring,可以在任何 Java EE 环境中使用,但 Spring 提供了对它的支持。
依赖注入总结
属性注入
属性注入是通过直接设置对象的私有字段来注入依赖项。这种方式相对简单直接,但在实践中并不常用,因为它违反了封装原则,使得类的内部状态可以被外部随意修改。
优点:
代码简洁,无需额外的构造方法或setter方法。
缺点:只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只有在使⽤的时候才会出现 NPE(空指
针异常)不能注⼊⼀个Final修饰的属性
破坏了封装性,使得类的内部状态可以被外部直接修改。
难以测试,因为依赖项在对象创建时就已经被注入,且无法替换为测试用的mock对象。
构造方法注入
构造方法注入是通过类的构造方法将依赖项注入到对象中。在创建类的实例时,必须提供所需的依赖项作为构造方法的参数。这种方式强制依赖项在对象创建时就必须被提供,从而确保依赖项不会被遗漏。
优点:
可以注⼊final修饰的属性;
注⼊的对象不会被修改;
依赖对象在使⽤前⼀定会被完全初始化,因为依赖是在类的构造⽅法中执⾏的,⽽构造⽅法
是在类加载阶段就会执⾏的⽅法;
通⽤性好, 构造⽅法是JDK⽀持的, 所以更换任何框架,他都是适⽤的;确保依赖项在对象被创建时就已经被注入,避免空指针异常。
使得对象一旦创建就处于可用状态。
有助于实现不可变对象(如果类的所有字段都在构造方法中设置)。
缺点:当依赖项较多时,构造方法可能会变得很长且难以阅读。
如果类的继承结构中有多个构造函数,需要小心处理构造函数链。
Setter注入
Setter方法注入是通过调用对象的setter方法来注入依赖项。这种方式允许对象在创建后,通过调用setter方法来设置其依赖项。
优点:
灵活性高,允许在对象创建后的任意时刻注入依赖项。
对于可选的依赖项特别有用,因为它们可以被省略而不影响对象的创建。
缺点:不能注⼊⼀个Final修饰的属性;
注⼊对象可能会被改变, 因为setter⽅法可能会被多次调⽤, 就有被修改的⻛险 ;对象可能处于不完全初始化状态,直到所有必要的依赖项都被注入。
依赖项可能没有被正确注入,需要额外的代码来检查依赖项是否已被注入。