1. Bean的定义与获取
1.1 定义Bean
在Spring 中定义Bean的方式主要有三种:
1、基于XML配置文件的方式(了解):通常会在配置文件中使用<bean>标签来定义Bean,并设置Bean的属性、依赖关系等信息。
2、基于注解的方式:在Java代码中使用注解来标识Bean,并指定Bean的属性、依赖关系等信息。常用的注解有@Component、@Controller、@Service等。
- @Component用于标识一个普通的 Bean 类,没有任何特殊的业务含义
- @Controller 用于标识一个控制器类、@Service 用于标识一个服务类,通常用于业务处理层、@Repository 注解用于标识一个数据访问类
- 除了这些区别,它们的使用方式是一样的,本质上都是将被注解的类交由 Spring 容器管理,供其他类去使用
3、基于Java Config的方式,在Java配置类中使用@Bean注解来定义Bean。当开发者想要使用的Bean是第三方组件时,不能在源码上标注@Component,可以使用@Bean注解的方式。
- @Bean注解通常放在方法上,用于指示该方法返回的实例应该被注册为Spring容器中的一个Bean
- 在方法上使用@Bean注解时,Spring框架会根据该方法的返回类型来自动创建并配置该Bean,同时也会根据方法的名称来为该Bean指定一个默认的名称
- 可以通过为@Bean添加属性值的方式为Bean定义一个新的名称
总的来说,不同的方式适用于不同的场景。在实际使用过程中,我们可以根据业务需求来选择最适合的方式来定义Bean,也可以兼用多种方式来定义Bean,这些方式也是可以共存的。
1.2 定义Bean示例
首先,在cn.obj包下声明BeanDefineConfig类,并在其中使用@Bean注解方式声明Bean:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
@Configuration
@ComponentScan("cn.highedu")
public class BeanDefineConfig {
/**
* 使用@Bean 显示声明Java Bean 组件
* Bean ID 为 names
* @return 创建的JavaBean
*/
@Bean
public ArrayList<String> names(){
ArrayList<String> names = new ArrayList<>();
names.add("Tom");
names.add("Jerry");
return names;
}
/**
* 使用 @Bean的属性设置BeanID
* @return 编程语言列表
*/
@Bean("languages")
public ArrayList<String> list(){
ArrayList<String> list = new ArrayList<>();
list.add("Java");
list.add("Java Script");
return list;
}
/**
* 使用@Bean 还可以在方法参数中引入其他的Bean
* 本例中引入了名为languages的Bean
* @param languages
* @return
*/
@Bean
public ArrayList<String> moreLanguages(ArrayList<String> languages){
ArrayList moreLanguages = new ArrayList(languages);
moreLanguages.add("Python");
moreLanguages.add("SQL");
return moreLanguages;
}
}
然后,在Application类中测试Bean的创建效果:
public class Application {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(BeanDefineConfig.class);
// 通过Spring IOC容器对象获取Bean的对象
ArrayList<String> names = (ArrayList<String>) context.getBean("names");
names.forEach(System.out::println);
System.out.println();
// 通过Spring IOC容器对象获取Bean的对象
ArrayList<String> moreCities = (ArrayList<String>) context.getBean("moreCities");
moreCities.forEach(System.out::println);
}
}
1.3 Spring框架中@Bean 和 @Component 的区别
在Spring框架中,@Bean注解和@Component注解均用于定义Bean,但是添加的位置和应用的场景不同。
1、@Bean注解主要用于在配置类中显式配置Bean。
- 可以添加在方法和注解前
- 将声明集中在一个(或几个)地方,方便统一管理Bean
- 可用于所有类(包括第三方工具包中的类)
2、@Component注解主要用于在类前隐式的配置Bean。
- 主要添加在类前
- 位置分散,不便于统一管理
- 适合非常快速的开发
3、实际应用中,通常是两种方式混合使用。
1.4 Spring IoC获取对象的方式
在Spring IoC容器中获取Bean可以通过以下几种方式:
1、context.getBean(Class<T> requiredType):根据类型获取Bean。其中:
- requiredType为要获取bean的类型
- 如果容器中有多个实现该类型的Bean,但是没有配置优先使用哪个Bean,则会抛出NoUniqueBeanDefinitionException异常
2、context.getBean(String name):根据名称获取Bean。其中:
- name为Bean在Spring IoC容器中的名称
- 默认情况下,Bean的名称是类名进行了首字母小写的处理后的结果
- 可以通过在Spring注解中设置名称来设置Bean的名称,如:@Component("myBean")
3、context.getBean(String name, Class<T> requiredType):根据Bean名称和类型获取Bean。
- 该方法相当于在getBean(String name)方法的基础上添加了Bean类型的验证
- 如果获取的Bean对象与requiredType指定的类型不同,将抛出BeanNotOfRequiredTypeException
4、context.getBeansOfType(Class<T> type):根据类型获取容器中所有实现该类型的Bean。
- type为要获取bean的类型
- 该方法返回Map类型,其中key为Bean在容器中的名称,value为Bean实例
1.5 IoC获取对象示例
在Application类中,测试IoC获取对象的方式:
public class Application {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(BeanDefineConfig.class);
// IoC容器中存在多个相同类型的Bean时,
// 使用.class获取会抛出异常:NoUniqueBeanDefinitionException
// ArrayList<String> names = (ArrayList<String>) context.getBean(ArrayList.class);
// 使用name+type的方式,不需要再进行强制类型转换
ArrayList<String> moreLanguages = context.getBean("moreLanguages", ArrayList.class);
moreLanguages.forEach(System.out::println);
// getBeansOfType方法返回的类型为Map
context.getBeansOfType(ArrayList.class).forEach((name, bean) -> {
System.out.println(name + " : " + bean);
});
}
}
2. Bean的作用域
2.1 Bean的作用域概述
Bean的作用域就是指Spring中Java Bean 有效范围,其中最为常用的作用域有2种:
- 单例作用域(singleton):Bean的默认作用域,任何时候获得的Bean对象都是同一个实例
- 原型作用域(prototype):每次引到bean时都会创建新的实例
可以通过@Scope注解来设置Bean的作用域,该注解可以添加在类前(与@Component注解搭配使用),也可以添加在方法前(与@Bean注解搭配使用)。
Bean的其他类型的作用域如下表所示(了解即可):
2.2 Bean的作用域示例
在com.obj包下新建SingletonBean:
import org.springframework.stereotype.Component;
@Component
public class SingletonBean {
@Override
public String toString() {
return "SingletonBean";
}
}
在com.obj.spring包下新建PrototypeBean:
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
public class PrototypeBean {
@Override
public String toString() {
return "PrototypeBean";
}
}
在com.obj包下新建ScopeTest类,对Bean的作用域进行测试:
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ScopeTest {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(ContextConfig.class);
// 测试默认的Scope
SingletonBean bean1 = context.getBean(SingletonBean.class);
SingletonBean bean2 = context.getBean(SingletonBean.class);
System.out.println("bean1 和 bean2 是否是同一个对象:" + (bean1 == bean2));
// 测试prototype
PrototypeBean bean3 = context.getBean(PrototypeBean.class);
PrototypeBean bean4 = context.getBean(PrototypeBean.class);
System.out.println("bean3 和 bean4 是否是同一个对象:" + (bean3 == bean4));
}
}
3. 依赖注入进阶
3.1 Spring支持的依赖注入方式
Spring框架主要支持三种注入方式:字段注入、构造器注入、setter方法注入。
1、字段注入(Field Injection):通过直接在类的字段上添加注解来注入依赖。
- 字段注入在实际使用中比较简便,但也可能降低代码的可测试性和清晰度,因为字段通常是私有的。
2、构造器注入(Constructor Injection):通过类的构造方法来注入依赖。
- 在创建对象实例时,容器会通过构造器参数来解析和注入依赖项。
- 构造器注入强制要求依赖在对象创建时就需要提供,适用于必须的依赖关系。
3、setter方法注入(Setter Injection):通过类的setter方法来注入依赖。
- 容器会调用目标类的setter方法,将依赖项注入到目标类的属性中。
- setter方法注入适用于可选性依赖或需要在对象创建后才能设置的情况。
3.2 构造注入示例
修改Student类的代码,增加ArrayList类型的属性、带参构造器及toString方法:
@Component
public class Student {
@Autowired
private Computer computer;
private ArrayList<String> languages;
public Student(){
System.out.println("Student无参构造方法被调用");
}
@Autowired
public Student(ArrayList<String> moreLanguages){
System.out.println("Student带参构造方法被调用");
this.languages = moreLanguages;
}
@Override
public String toString() {
return "Student{computer=" + computer + ", languages=" + languages + '}';
}
public void use(){
System.out.println("使用电脑:"+computer);
}
}
在Application类中获取Student类的对象并输出。注意,本例中的配置类需要使用BeanDefineConfig。
public class Application {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(BeanDefineConfig.class);
Student student = context.getBean(Student.class);
System.out.println(student);
}
}
控制台输出如下:
从输出结果可知,Spring框架调用了Student类的带参构造器来创建Student对象,并且自动从容器中查询到名为moreLanguages的Bean对象,注入到该构造器中。
接下来做一个实验,将Student类中带参构造器的参数名改为test。
再次运行Application类的main方法,程序抛出NoUniqueBeanDefinitionException异常。该异常的原因是Spring无法通过test这个名称从容器中找到Bean对象,所以改用ArrayList类型进行查找,找到了不止一个Bean对象,所以抛出异常。
此处可以看到,采用构造器注入的方式,如果构造器中要求注入的Bean存在问题,则会导致Student对象构造出错。因此,构造器注入适用于必须的依赖关系。
3.3 setter方法注入示例
修改Student类的代码,增加setLanguages方法,并删除带参构造器前的@Autowired注解:
public Student(ArrayList<String> moreLanguages){
System.out.println("Student带参构造方法被调用");
this.languages = moreLanguages;
}
@Autowired
public void setLanguages(ArrayList<String> moreLanguages){
System.out.println("setLanguages方法被调用");
this.languages = moreLanguages;
}
运行Application类的main方法,查看控制台输出的内容。
可以看到,移除带参构造方法前的@Autowired注解后,Spring默认调用无参构造方法来创建Student对象,然后调用set方法注入依赖。因此,set方法的注入时机晚于构造方法。
在Student类中新增一个String类型的属性,添加对应的set和get方法,并在set方法前添加@Autowired(required=false)注解。
@Component
public class Student {
// 通过Spring框架获取Computer对象
@Autowired
private Computer computer;
private ArrayList<String> languages;
private String name;
@Autowired(required = false)
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
// 省略该类中其他内容
在Application类中增加对getName方法的调用。
public class Application {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(BeanDefineConfig.class);
Student student = context.getBean(Student.class);
System.out.println(student);
System.out.println(student.getName());
// 省略该类中其他内容
执行程序的main方法,可以看到程序正常执行,getName方法返回的结果为null。此处主要演示的是@Autowired(required = false)的执行逻辑,即如果能找到相关符合条件的Bean对象,则执行注入,反之则不执行注入。这种注入方式适用于那些可选的依赖。
3.4 为什么Spring不建议使用字段注入
1、Spring框架支持三种主要的注入方式:字段注入、构造器注入、setter方法注入。其中,字段注入是指在类的字段上添加注解来注入依赖。
2、可能导致空指针异常:一个Bean的初始顺序为静态变量或静态代码块 > 实例变量或初始化语句块 > 构造方法 > 字段注入。在静态代码块、初始化语句块、构造方法中使用@Autowired标记的字段,会引起空指针异常。
3、不利于测试: 在单元测试中,为了隔离被测试类与外部依赖之间的耦合,常常需要模拟依赖对象。字段注入会使得测试类无法直接通过构造器或方法注入模拟对象,从而增加了测试的复杂性。
4、无法注入 final 字段: 字段注入无法用于注入 final 字段,这会限制一些设计和测试的可能性。
3.5 @Autowired的Bean匹配机制
@Autowired的Bean匹配机制是指在执行依赖注入之前,Spring容器会根据被注入字段、方法参数的类型来查找匹配的 Bean。
该机制的执行顺序为:
1、先根据类型匹配
- 若没有匹配类型,则注入失败
- 有匹配类型,对应的实例有1个,则注入
2、如果匹配类型的实例有多个,则查看优先级注解:
- 查看是否有@Qualifier注解,有则按其指定规则匹配
- 查看是否有@Primary注解,有则按其指定规则匹配
3、如果类型匹配的实例有多个,且无法选出唯一的,则转为根据Bean的名称匹配
- 如果名称匹配成功就注入,反之注入失败
在entity包下声明DemoBean类。
@Component
public class DemoBean {
@Autowired
private ArrayList<String> list;
@Override
public String toString() {
return "DemoBean{" +
"list=" + list +
'}';
}
}
在Application类中获取DemoBean的对象并输出。
public class Application {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(BeanDefineConfig.class);
DemoBean demoBean = context.getBean(DemoBean.class);
System.out.println(demoBean);
}
}
此时,程序抛出NoUniqueBeanDefinitionException,表示有多个匹配的实例,但是无法确定该使用哪一个。
接下来,将list属性的名称修改为moreLanguages。
@Autowired
private ArrayList<String> moreLanguages;
@Override
public String toString() {
return "DemoBean{" + "list=" + moreLanguages + "}";
}
运行Application类,可以发现,依赖注入成功,根据属性名匹配了名为moreLanguages的Bean。
然后,在BeanDefineConfig类的names方法前添加@Primary注解。
@Bean
@Primary
public ArrayList<String> names(){
ArrayList<String> names = new ArrayList<>();
names.add("Tom");
names.add("Jerry");
return names;
}
运行Application类,可以看到,依赖注入成功。从结果可以看出,@Primary注解的优先级高于按属性名匹配。
接下来,在DemoBean类的list属性前添加@Qualifier("languages")注解,再次运行测试用例,查看效果。
此处可以发现,@Qualifier("languages")注解的优先级高于@Primary注解。
综上,如果存在多个匹配的Bean,可以通过三种方式解决:@Qualifier注解,@Primary注解或使用Bean名称匹配。这三种方式的优先级为:@Qualifier > @Primary > Bean名称。