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

本文内容纲要:SpringBoot" title="SpringBoot">SpringBoot系列: 理解 Spring 的依赖注入(二)

==============================

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 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

本文内容总结:SpringBoot系列: 理解 Spring 的依赖注入(二)

原文链接:https://www.cnblogs.com/harrychinese/p/spring_ioc2.html

以上是 SpringBoot系列: 理解 Spring 的依赖注入(二) 的全部内容, 来源链接: utcz.com/z/362635.html

回到顶部