SpringBoot系列: 理解 Spring 的依赖注入(二)

时间:2024-04-09 16:07:33

==============================
Spring 容器中 Bean 的名称
==============================
声明 bean 有两个方式, 一个是 @Bean, 另一个是 @Component 和它的子类 (包括 @Service/@Controller/@Repository/@Configuration), Spring 容器中 bean 名生成规则分两大类, 分别是:

一. @Component 和它的子注解是用来注解 Class 的. 这些注解标注 Bean 的命名规则是:
1. 如果 @Component 指定了参数值的话, 参数值就是 bean 名称.
2. 如果 @Component 未指定参数值的话, bean 名就要看被注解的类名, 如果类名开头是两个或两个大写字母, bean 名同类名完全一致; 如果开头只有一个大写字母, bean 名是类名首字母小写版.

@Component 和它的子注解虽然可以加在接口上, 但不推荐这样, 应该直接加在具体的实现类上, 实际项目中, Service 层往往有 Interface 和 impl, 应该在 impl 上加 @Service 注解.

二. @Bean 注解往往用来标注一个函数, @Bean 注解标注 Bean 的命名规则是:
1. 如果 @Bean 指定了 name 参数, 以参数为准.
2. 未指定 name 参数的情况下, 以方法名作为 bean 的名称.

==============================
Bean 作用域
==============================
参考: https://www.jianshu.com/p/502c40cc1c41
Bean 常用的作用域有下面 5 类,
@Scope("singleton")
@Scope("prototype")
@Scope("request")
@Scope("session")
@Scope("global-session")

1. @Scope("singleton") 标注的 bean, 表示在 Spring 容器中只创建一个 bean 实例, 一般情况下在应用程序启动的时候, 就已经完成 bean 实例化了. 在程序运行过程中, 每次调用这个 bean 将使用同一个实例. 这也是 Spring 默认的 scope.
2. @Scope("prototype") 标注的 bean, 每次获取这个 bean 都创建一个新的实例.
3. @Scope("request"), 仅适用于 Web 项目, 给每个 http request 新建一个 Bean 实例.
4. @Scope("session"), 仅适用于 Web 项目, 给每个 http session 新建一个 Bean 实例.
5. @Scope("global-session"), 仅适用于 portal Web 项目, 给每个 global http session 新建一个 Bean 实例.

singleton bean 默认都是在容器创建后即初始化, 基本上等同于启动一个 JVM 后即创建 bean 实例, 如果应用很关注启动时间, 可以在声明 bean 的时候再加一个 @Lazy 注解, bean 实例化将延迟到第一次获取 bean 对象时. 一般情况下, 强烈建议所有的 Singleton bean 不要启用延迟加载, 这样就能在程序启动的时候发现问题.

==============================
不同 scope bean 的线程安全性问题
==============================
Struts2 每次请求过来都会实例化一个对象来响应, 所以没有线程安全问题, 而 Spring MVC 是基于视图函数做拦截的, 每次web请求过来后, Spring 都是使用 controller的指定方法去响应,  而不是新实例化一个controller.  Spring 这样做的好处是: 避免频繁对象实例化和GC, 效率会比Struts2要高; 但也不是没有缺点的, 会有线程不安全的潜在危险.  潜在的风险点是, 如果我们的Controller/Service/Repository类中包含实例变量, 就很可能会引起线程不安全. 避免线程不安全的方法有两个: 第一, Controller/Service/Repository类中不要加实例变量, 这是推荐做法. 第二, 默认情况下Spring IOC管理的bean, 其生命周期是  singleton,   我们可以修改 Controller/Service/Repository 的声明周期为  request.

Spring 默认的 scope 时 singleton, 因为不需要频繁的对象创建和内存回收, 性能最好, 但需要注意线程安全问题.
prototype 类型的 bean, 每次注入的都是一个全新对象, 显然没有线程安全问题.
request 和 session 对象, 除非我们在同一个 request 或 session 中显式地使用了多线程, 需要注意一下线程安全问题, 在绝大多数情形下, 这两个 scope 类型的 bean 是线程安全的.

对于 singleton 类型的 bean, 如何做到线程安全呢? 一般有两个方法:
1. bean 类中压根不定义成员变量, 所有的bean都是无状态的, 这样就杜绝了线程不安全的隐患.
2. 如果 bean 类必须有成员变量, 一定要将成员变量加到 ThreadLocal 中.
或者, 干脆将 singleton scope 改成 prototype.

==============================
Bean 对象的初始化钩子
==============================
设想如下场景:
1. 我们需要注入一个 bean 对象, 并要对该 bean 对象做一些特别设置, 而这个 bean 对象类的代码又不归我们管.
2. 对于 Boss 这个对象, 注入很多种 bean 对象 (比如 car/office 等), 如何在一处代码中集中为 car/office 对象做一些特别设置?

显然这些场景, 无法通过 bean 类的构造子实现, Spring 项目中, 我们可以使用下面两种方式实现:
1. JSR-250 定义的标准有 @PostConstruct 以及 @PreDestroy 注解, 这两个注解一般和 @Component 搭配使用.
2. Spring 提供的 @Bean 注解有 initMethod 和 destroyMethod 属性. 更推荐使用 @PostConstruct

==============================
@Autowired Bean 注入
==============================
@Autowired 可以对类成员变量、方法和构造函数加注解, Spring会自动扫描所有打了@Autowired注解的变量/方法/构造子, 完成自动装配任务. 换句话讲, 所有打了@Autowired注解的方法/构造子, Spring会在合适的时机自动调用它们, 一般情况下不需要我们再手工调用.

1. 在类成员变量上加标注, 可以省去 setter 方法.

public class Boss {
@Autowired
private Car car;
}

2. 在类的方法上加标注, Spring 自动完成"所有 bean"实参装配, 该方法会被Spring在合适的时机自动调用.

public class Boss {
private Car car;
private Office office; @Autowired
public void Init(Car car, Office office) {
this.car = car;
this.office = office ;
}
}

3. 在类的构造函数上加标注, Spring 自动完成"所有 bean"实参装配.

public class Boss {
private Car car;
private Office office; @Autowired
public Boss(Car car, Office office) {
this.car = car;
this.office = office ;
}
}

4. @Autowired 注解标在集合上或数组上的含义: 是用来获取同一个接口的多个实现.
摘自 https://www.chkui.com/article/spring/spring_core_auto_inject_of_annotation

interface A {}

@Component
class implA1 implements A{} @Component
class implA2 implements A{} class MyClass {
@Autowired
private A[] a;
@Autowired
private Set<A> set;
@Autowired
private Map<String, A> map;
}

使用 Map 时,key 必须声明为 String,在运行时会 key 是注入 Bean 的 name/id.

==============================
JSR-250/JSR-330 的 Bean 注入
==============================
JSR-250 标准的注入注解是 @Resource.
JSR-330 标准的注入注解是 @Inject.
Spring 专有的注入注解是 @Autowired.

@Inject 注解和 @Autowired 几乎一样, 其功能比 @Autowired 稍弱一些 (比如没有 required 属性), 所以不推荐使用.
在实际项目中, 经常使用到的是 @Autowired 和 @Resource.
@Autowired 默认是 byType 注入的, 而 @Resource 本身有 name 和 type 属性, 默认是按照 byName 注入的.

@Autowired 和 @Inject 注入 bean 对象的执行路径是:
1.Match by Type : 优先 byType 注入
2.Restricts by Qualifier : 依照 @Qualifier 指定的 name 来注入, 但如果没有注入成功, 直接报错.
3.Match by Name : 最后按照默认的 name 注入.

@Resource 注入 bean 对象的执行路径是:
1. 如果同时指定了 name 和 type, 则在容器中找 name 和 type 都匹配的 bean 进行装配, 找不到则抛出异常.
2. 如果指定了 name, 则在容器中查找名称 (id) 匹配的 bean 进行装配, 找不到则抛出异常.
3. 如果指定了 type, 则在容器中找到类型匹配的唯一 bean 进行装配, 找不到或者找到多个, 都会抛出异常.
4. 如果既没有指定 name,又没有指定 type 时, 将按照下面执行路径:
4.1 按照默认的名字注入.
4.2 按照类型注入.
4.3 依照 @Qualifier 的修饰来注入.

参考:
http://javainsimpleway.com/autowired-resource-and-inject-2/
http://einverne.github.io/post/2017/08/autowired-vs-resource-vs-inject.html

==============================
@Primary 解决多个子类无法注入问题
==============================
有时候在我们的项目中, 同一个接口可能会有多个实现 (或子类), 在使用 @Autowired 注入 bean 实例时, 会报错. 原因很简单, @Autowired 默认是按照 type 注入的, 现在有多个子类, Spring IoC 容器不知道该如何注入了.

解决方式式在声明 Bean 的时候, 可以再多加一个 @Primary 注解. @Primary 既可和 @Component 搭配使用, 也可以和 @Bean 搭配使用.

@Component
@Primary
public class BenzCar implements ICar {
public void run() {
System.out.println("Benz run");
} @Override
public String toString() {
return "Brand: Benz, price:1000";
}
} @Component
public class VWCar implements ICar {
public void run() {
System.out.println("VW run");
} @Override
public String toString() {
return "Brand: Volkwargon, price:1000";
}
} @Component
public class Boss {
@Autowired
private ICar car;
} @Configuration
@ComponentScan("javaTestMaven.demo2")
public class SomeConfig {
} public class App {
public static void main(String[] args) {
App app=new App();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(SomeConfig.class);
context.refresh();
Boss boss = context.getBean(Boss.class);
System.out.println(boss);
context.close();
}
}

==============================
@Qualifier 的使用
==============================
上个例子中, 使用了 @Primary 注解来解决多个子类无法注入的情况, 其实 Spring 还提供其他办法, @Qualifier 注解就是其中之一.
一旦加上了 @Qualifier 之后, @Autowired 在 byType 注入失败后, 将按照 @Qualifier 设定的参数来注入. @Qualifier 可以和 @Autowired/ @Resource/ @Inject 一起使用, 但多数情况下是和 @Autowired 一起使用.

@Autowired 的标注对象是成员变量、方法、构造函数.
@Qualifier 的标注对象是成员变量、方法入参、构造函数入参.
上面表述看上去是好像一样, 其实是有差别的, 重点是两个注解修饰的主体是不同的, 以注解方法为例, @Qualifier 修饰的是单个实参, 而 @Autowired 修饰的是方法体. 如果该方法有多个 bean 类的形式参数, 每一个参数都可使用 @Qualifier 修饰.

下面是一个 @Qualifier 用在构造参数实参的示例.

public class Boss {
private Car car;
private Office office; @Autowired
public Boss(Car car, @Qualifier("office") Office office){
this.car = car;
this.office = office ;
}
}

==============================
@Conditional 实现条件注入
==============================
@Primary 的缺点: 通过类型方式将接口和某个具体实现绑定死了, 好处:我们可以随时切换 Primary 类, 达到切换具体实现.
@Qualifier 的缺点: 通过名称方式将接口和具体实现的名称绑定死了, 好处:我们可以随时调整名称, 达到切换具体实现.
但总体来讲 @Primary 和 @Qualifier 都有点 hard code 味道, 有什么更好的方案呢? 答案就是 @Conditional, 使用起来稍微复杂些, Spring Boot 中大量使用了 @Conditional 注解, 比如多 profile 环境.

首先我们需要实现 Spring 的 Condition 接口, 然后在 Config 类中使用 @Conditional + @Bean 组合声明 bean 类型. 加上 @Conditional 之后, 将在 bean 实例化时按实际的条件 evaluate 来确定使用哪个子类.

public class LinuxCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
boolean result=context.getEnvironment().getProperty("os.name").contains("Linux");
return result ;
}
} public class WindowsCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
boolean result=context.getEnvironment().getProperty("os.name").contains("Windows");
return result ;
}
} @Configuration
@ComponentScan("javaTestMaven.demo2")
public class SomeConfig {
@Bean
@Conditional(LinuxCondition.class)
public ICar getBenzVWCar() {
return new VWCar();
} @Bean
@Conditional(WindowsCondition.class)
public ICar getBenzCar() {
return new BenzCar();
}
} public class App {
public static void main(String[] args) {
App app=new App();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(SomeConfig.class);
context.refresh();
ICar car = context.getBean(ICar.class);
System.out.println(car);
context.close();
}
}

==============================
Spring 的其他注解
==============================
一. @Autowired(required=false)
@Autowired() 如果不指定 required 属性, 相当于 required 属性为 true, 意思是被注入的对象不能为 null, 如果设定 required=false, 被注入的对象可以为 null.
因为 @Autowired(required=false) 容许注入 null, 会给程序带来潜在的问题, 所以仅仅在开发和测试阶段使用, 比如被依赖的类还没被声明成 bean.

二. @Required
依赖注入主要是两种方式, 通过构造子注入或通过 Setter 函数注入, 一般地我们会将必需的依赖项通过构造子注入,对于非必需的依赖项通过 Setter 函数注入.
Spring 提供了 @Required 注解, 可以将某个 Setter 注入上升到必需级别.
@Required 用来注解 Setter 方法, 只适用于基于 XML 配置的 setter 注入方式, 效果和 @Autowired(required=true) 一样, 推荐使用后者.

三. @DependsOn
直接依赖关系推荐使用构造子和 Setter 函数设置, 但对于没有直接依赖的对象或依赖关系不明显, 可以使用 @DependsOn.

四. @Order
@Order 一般用在控制多个子类的 bean 对象实例化的顺序, @Order() 注解的参数值越小, 实例化越早.
在下面例子中, lst 中的第一个元素是 implA2 对象, 第二个元素是 implA1 对象.

interface A {}

@Component
@Order(2)
class implA1 implements A{} @Component
@Order(1)
class implA2 implements A{} class MyClass {
@Autowired
private List<A> lst;
}

===============================
参考
===============================
https://www.chkui.com/article/spring/spring_core_auto_inject_of_annotation
https://www.ibm.com/developerworks/cn/java/j-lo-spring25-ioc/
https://www.ibm.com/developerworks/cn/java/j-lo-spring25-mvc/
https://www.baeldung.com/spring-autowire