avatar

目录
Spring学习 依赖注入

依赖注入

Been的三种装配机制

  1. 在XML中进行显示装配
  2. 在Java中进行显示装配
  3. 隐式的bean发现机制和自动装配

    隐式的bean发现机制和自动装配

    我们先来谈谈第三条。
    Spring从两个角度来实现自动化装配
  4. 组件扫描:Spring会自动发现应用上下文中所创建的bean
  5. 自动装配:Spring自动满足bean之间的依赖

实现的流程如下,其中1,2步实现的是组件扫描,第三步则实现了自动装填

  1. 首先需要创建可被发现的bean,方式如下
    基础版本

    package soundsystem;
    import org.springframework.stereotype.Component;
    @Component
    public class SgtPeppers {
    }
    

    为bean命名的版本

    package soundsystem;
    import org.springframework.stereotype.Component;
    @Component("SgtPeppers")
    public class SgtPeppers {
    }
    
  2. 由于组件扫描是默认不存在的,所以需要显示配置Spring,让他开启组件扫描的功能。有两种配置方式:

    • 基于java的配置,方式如下

      基础版本,该版本会扫描与配置类相同的包

      package soundsystem;
      import org.springframework.context.annotation.componentScan;
      import org.springframework.context.annotation.Con;
      
      @Configuration  //告诉Spring这个java类是配置文件
      @ComponentScan  //启动组件扫描器
      
      public class CDplayerConfig {
      }
      

      设置组件扫描的基础包的版本,该版本会扫描指定的基础包及其子包

      package soundsystem;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.ComponentScan;
      import org.springframework.context.annotation.Configuration;
      
      @Configuration  //告诉Spring这个java类是配置文件
      
      /*可以扫描多个包,当扫描多个包时推荐这种方式,因为这是类型安全的*/
      @ComponentScan(basePackages={CDPlayer.class,DVDPlayer.class})
      
      /*可以扫描多个包,不推荐这种*/
      //@ComponentScan(basePackages={"soundsystem","video"})
      
      /*扫描一个包*/
      //@ComponentScan(basePackages="soundsystem")
      
      /*扫描一个包*/
      //@ComponentScan("soundsystem")
      public class CDplayerConfig {
          @Bean
          public CompactDisc sgtPeppers(){
              return new SgtPeppers();
          }
      
          @Bean
          public CDPlayer cdPlayer(CompactDisc compactDisc){
              return new CDplayer(compactDisc);
          }
      }    
      
    • 基于xml的配置

  3. 在创建完可被发现的Bean和配置好文件后,在运行时我们便可以进行自动装配了,简单来说,自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean需求的其他bean。其实现方法如下,需运用到注解@Autowired

    package soundsystem;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    @Component
    public class CDplayer {
        private CompactDisc cd;
    
        /*
            对cd参数进行自动装配,
            如果没有匹配的bean的话,
            Spring将会让这个bean处于未装配的状态,
            且在装配时不抛出异常,
            在运行时会抛出NullPointerException异常
        */
        //@Autowired(required=false)
    
        /*对cd参数进行自动装配*/
        @Autowired
        public CDplayer(CompactDisc cd){
            this.cd = cd;
        }
    
        /*不仅可以用在构造器上,还可以用于类的任何方法*/
        @Autowired
        public void setCompactDisc(CompactDisc cd) {
            this.cd = cd;
        }
        public void play(){
            cd.play();
        }
    }  
    

自动化装配之后的显示装配

对于第三方类库等其他情况而言,你不能像对待自己创建的java类一样为其添上@Component注解,所以在此类情况下,你就需要用到显示装配了。
而显示配置有两种方式:

  1. 基于JAVA
  2. 基于XML

基于JAVA的显示装配

要在JavaConfig中声明bean,我们需要编写一个方法,这个方法会创建所需要的实例,然后给这个方法添加@Bean注解(PS:该方法不必是构造器方法,其他能产生实例的方法均可,所以他比XML灵活得多)方法如下

基础版本

@Bean
Public CompactDisc sgtPeppers(){
    return new SgtPeppers();
}

可以为bean取名的版本

@Bean
Public CompactDisc sgtPeppers(){
    return new SgtPeppers();
}

以上版本的bean在创建时并没有其他bean的依赖,但如果有依赖呢,以下是一个基础版本

@Bean
Public CDPlayer cdPlayer(){
    return new CDplayer(sgtPeppers());
}  

可以看出这个基础版本给人一种很奇怪的感觉,在构造器方法里还调用了一个方法着实令人不舒服,(ps:由于sgtPeppers()方法上添加了@bean注解,Spring将会拦截所有对他的调用,并确保直接发返回该方法所创建的bean,而不是每次都对其进行实际的调用)
所以推荐第二种版本
通过参数来构造对象的版本

@Bean
Public CDPlayer cdPlayer(CompactDisc compactDisc){
    return new CDplayer(compactDisc);
}

当Spring调用上述方法创建bean时,他会自动装配一个CompactiDisc到配置方法之中,方法体就可以按照合适的方法来使用他。而不需要调用类似sgtPeepers的@Bean方法来构造了。

基于XML的显示装配

导入混合配置

虽然Bean有三种装配方式,但他们彼此并不冲突,并且可以相互配合,一般有四种方式的混合。

  1. 一个JavaConfig混合另一个JavaConfig(使用@import注解)
  2. 一个JavaConfig混合另一个XML(使用@importResource注解)
  3. 一个XML混合一个JavaConfig
  4. 一个XML混合另一个XML

以下是这四个的示例

  1. 一个JavaConfig混合另一个JavaConfig

    • 导入单个JavaConfig

      package soundsystem;
      
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.context.annotation.Import;
      
      @Configuration
      @Import(CDplayerConfig.class)
      public class SoundSystemConfig {
          @Bean
          public CompactDisc sgtPeppers(){
              return new SgtPeppers();
          }
      }
      
* 导入多个JavaConfig  

        package soundsystem;

        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.context.annotation.Import;

        @Configuration
        @Import({CDplayerConfig.class,CDConfig.class})
        public class SoundSystemConfig {
            @Bean
            public CompactDisc sgtPeppers(){
                return new SgtPeppers();
            }
        }
  1. 一个JavaConfig混合另一个XML

    package soundsystem;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Import;
    import org.springframework.context.annotation.ImportResource;
    
    @Configuration
    @Import(CDplayerConfig.class)
    @ImportResource("classpath:cd-config.xml")
    public class SoundSystemConfig {
        @Bean
        public CompactDisc sgtPeppers(){
            return new SgtPeppers();
        }
    }
    
  2. 一个XML混合一个JavaConfig

  3. 一个XML混合另一个XML

高级装配

只使用以上的普通装配,可能会遇上一些问题,例如:

  1. 如何根据环境(开发环境,测试环境,生产环境)来自动切换生成的Bean
  2. 如何根据相应条件来生成Bean(在满足某个特定的情况下才生成)
  3. 如何解决自动装配的歧义性
  4. 如何在运行时值注入

以下是四个对应的高级装配技巧

如何根据环境(开发环境,测试环境,生产环境)来自动切换生成的Bean

解决这个情景需要两步,一步是配置Bean,一步是激活相应环境(激活profile)

  1. 配置Bean(分别有两种方法,一种是基于JavaConfig,一种是基于XML的)
    • 基于JavaConfig(使用@Profile注解)
      将@Profile注解运用在配置类上
        package soundsystem;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.context.annotation.Profile;

        @Configuration
        @Profile("dev")  //用于开发环境
        //@Profile("prod") 用于生产环境
        //@Profile("qa") 用于测试环境
        public class DevelopmentProfileConfig {
            @Bean
            public CompactDisc sgtPeppers(){
                return new SgtPeppers();
            }

        }  

    将@Profile注解运用在方法上(单个Bean上)  

        package soundsystem;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.context.annotation.Profile;

        @Configuration
        public class DevelopmentProfileConfig {
            @Bean
            @Profile("dev")
            public CompactDisc sgtPeppers(){
                return new SgtPeppers();
            }

            @Bean
            //@Profile("prod")
            public CDPlayer cdPlayer(CompactDisc compactDisc){
                return new CDplayer(compactDisc);
            }
        }



* 基于XML
  1. 激活profile
    Spring在确定那个profile处于激活状态是,会依赖两个独立的属性

    • spring.profiles.active
    • spring.profiles.default

      Spring会优先查看active,然后查看default,如果两者均没有设置的话,那就没有激活的profile,因此只会创建没有定义在profile中的bean
      有多种方式来设置这两个属性:

    • 作为DispatcherServlet的初始化参数
    • 作为Web应用的上下文参数
    • 作为JNDI条目
    • 作为环境变量
    • 作为JVM的系统属性
    • 在集成测试类上,使用@ActiveProfiles注解设置

      介绍一下第一种和最后一种,
      第一种:在Web应用的web.xml文件中设置默认的profile(注意context-param和servlet元素)

      <?xml version=”1.0” encoding=”UTF-8”?>
      <web-app xmlns=”http://xmlns.jcp.org/xml/ns/javaee"

      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
      version="4.0">
      

      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring/root-context.xml</param-value>
      

      <param-name>spring.profiles.default</param-name>
      <param-value>dev</param-value>
      

      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      


      <servlet-name>appServlet</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      <init-param>
          <param-name>spring.profiles.default</param-name>
          <param-value>dev</param-value>
      </init-param>
      


      <servlet-name>appServlet</servlet-name>
      <url-pattern>/</url-pattern>
      


      最后一种:在测试类中使用@ActiveProfiles(“dev”)注解来表示激活dev环境

如何根据相应条件来生成Bean(在满足某个特定的情况下才生成)

在Spring4之前,很难实现这种级别的条件化配置,但是Spring 4引入了一个新的@conditional注解,它可以用到带有@Bean注解的方法上,如果给定的条件计算结果为true,就会创建这个bean,否则的话,这个bean会被忽略。例子如下

@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean{
    return new MagicBean();
}

可见其为@Conditional注解传入了一个class对象,该对象要求实现Condition接口,以完成条件的检查
Condition接口的定义如下

public interface Condition{
    boolean matches(ConditionContext ctxt, AnnotatedTypeMetadata metadata);
}

而实现该接口的检查类如下

package soundsystem;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ClassUtils;

public class MagicExistsCondition implements Condition {
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata){
        Environment env = context.getEnvironment();
        return env.containsProperty("magic"); //检查magic属性
    }
}

如何解决自动装配的歧义性

如果有多个Bean能够匹配结果的话,这种歧义性会阻碍Spring自动装配属性、构造器参数或方法参数。例如当一个Bean的某个方法需要传入一个实现了某接口的Bean,但有多个Bean实现了该接口,那么自动装配便会出现歧义性,并且会抛出noUniqueBeanDefinitionException异常。
解决的办法是使用

  1. 标示首选的bean(使用@Primary注解)
  2. 限定自动装配的bean(使用@Qualifier注解)

以下是对应的示例

  1. 标示首选的bean(使用@Primary注解)

    @Component
    @Primary
    public class Cake implements Dessrt{ ... }
    
    @Component
    public class Cookies implements Dessrt{ ... }
    
    @Component
    public class IceCream implements Dessrt{ ... }
    

    这样在某个方法需要传入某个实现了Dessrt的Bean时,会首选第一个 Cake Bean。
    但这里也有个问题,当上述的例子出现2个或更多@Primary注解时,依旧会出现歧义性,
    所以有了第二种方法

  2. 限定自动装配的bean(使用@Qualifier注解)
    而这里又有三种方法来实现

    1. 普通方式
      该方法是将注解放在需要自动装配的地方。例子如下

      @Autowired
      @Qualifier("iceCream") //传入想要注入的bean的限定符
      public void setDessert(Dessert dessert) {
          tis.dessert = dessert;
      }
      

      上例所引用的bean要具有String类型的”iceCream”作为限定符。如果没有指定其他的限定符的话,所有的bean都会给顶一个默认的限定符,这个限定符与bean的ID相同。
      基于默认的限定符虽然非常简单,但如果你重构了该类,例如重命名该类,则传入的@Qualifier注解的字符串限定符也需要改
      所以建议在创建bean时自己定义它的限定符.且最好是该类的特征而不是随意取名,这是个最佳实践,而这就是第二个方法,创建自己的限定符

    2. 创建自定义的限定符
      该方法是将@Component与@Qualifier注解组合使用
      定义bean文件

      @Component
      @Qualifier("cold")
      public class IceCream implements Dessert { ... }  
      

      使用

      @Autowired
      @Qualifier("cold") //传入想要注入的bean的限定符
      public void setDessert(Dessert dessert) {
          tis.dessert = dessert;
      }  
      

      但这里依然有个问题,基于特征来描述限定符往往不能精确到一个bean。例如你可以为
      drink been、ice been都给予@Qualifier(“cold”)注解,但这时在自动装配时又会产生歧义性。你也许考虑在一个bean中运用多个@Qualifier注解,如

      @Component
      @Qualifier("cold")
      @Qualifier("sweet")
      public class IceCream implements Dessert { ... } 
      

      但不幸的是,Java不允许在同一个条目上重复出现相同类型的多个注解,所以有了第三种方法

    3. 使用自定义的限定符注解
      创建一个@Cold注解

      import org.springframework.beans.factory.annotation.Qualifier;
      
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      @Target({ElementType.CONSTRUCTOR,ElementType.FIELD,
              ElementType.METHOD,ElementType.TYPE})
      @Retention(RetentionPolicy.RUNTIME)
      @Qualifier
      public @interface Cold {}
      

      创建另一个@Creamy注解

      import org.springframework.beans.factory.annotation.Qualifier;
      
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      @Target({ElementType.CONSTRUCTOR,ElementType.FIELD,
              ElementType.METHOD,ElementType.TYPE})
      @Retention(RetentionPolicy.RUNTIME)
      @Qualifier
      public @interface Creamy {}
      

      在定义bean的文件中使用注解

      @Component
      @Cold
      @Creamy
      public class IceCream implements Dessert { ... } 
      

      在自动装配的地方使用注解

      @Autowired
      @Cold
      @Creamy
      public void setDessert(Dessert dessert) {
          tis.dessert = dessert;
      } 
      

      这样IceCream bean就可以自动注入该方法中。
      还需提一点,使用自定义的限定符注解相对于使用原始的@Qualifier并借助String类型来制定限定符,自定义的注解也更为类型安全。

运行时值注入

上面自动装配的技术都只是支持了自动装配bean,但如何让值在运行时注入呢?想让值在运行时再确定。为了实现这些功能,Spring提供 两种在运行时求值的方法:

  • 属性占位符
  • Spring表达式语言

其中由以Spring表达式功能更为强劲。
以下是针对上面两个方法的示例

  • 属性占位符
    使用@PropertySource注解以及env环境变量

    package soundsystem;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.core.env.Environment;
    
    @Configuration
    @PropertySource("classpath:/com/soundsystem/app.properties") //声明属性源
    public class ExpressiveConfig {
        @Autowired
        Environment env;
    
        @Bean
        public BlackDisc disc(){
            return new BlankDisc{
                env.getProperty("disc.title"),
                env.getProperty("disc.artist")
            };
        }
    }
    

    直接从Environment中检索属性是非常方便的,尤其是在jaca配置中配置bean的时候
    但是,Spring也提供了通过占位符装配属性的方法,这些占位符的值会来源一个属性域(占位符形式为${…})
    使用${ … } 以及一个PropertySourcesPlaceholderConfigurer(推荐使用,因为其基于Spring Environment及其属性元来解析占位符)或者一个PropertyPlaceholderConfigurer

    在java配置类中定义一个 PropertySourcesPlaceholderConfigurer

    @Bean
    public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
        return new PropertySourcesPlaceholderConfigurer();
    }
    

    运行时值注入

    public BlankDisc(
            @Value("${disc.title}") String title,
            @Value("${disc.artisit}") String artist){
        this.title = title;
        this.artist = artist;
    }
    
  • Spring表达式语言(SpEL)
    上述的占位符还是不够灵活,例如你想运用一个bean的属性加上另一个常量,将这个值动态注入时,就会很棘手,所有此时出现了Spring表达式语言,他更加强大,更加灵活。

    SpEL有很多特性,包括:

    • 使用bean的ID来应用bean;
    • 调用方法和访问对象的属性;
    • 对峙进行算数、关系和逻辑运算;
    • 正则表达式匹配;
    • 集合操作;

      SpEL将表达式放入#{ … }中
      举几个例子

    • 引用bean、属性和方法:通过ID引用其他的bean

      //引用artist属性
      #{sgtPeppers.artist}
      
      //引用selectArtist()方法
      #{sgtPeppers.selectArtist()}
      
* 在表达式中使用类型:依赖T()运算符

        #{T(java.lang.Math).random()}

* 使用正则表达式  

        #{admin.email mathes 'www.admin@email.com'}

* 计算集合  

        //获取元素
        #{"This is a test"[3]}

        //提供查询运算符(.?[])用来对集合进行过滤  
        #{jukebox.songs.?[artist eq 'Aerosmith']}

        //提供另外两个查询运算符:".^[]"和".$[]"分别用来在集合中查询第一个匹配项和最后一个匹配项
        #{jukebox.songs.$[artist eq 'Aerosmith']}

        //提供投影运算符(.![]),他会从集合的每个成员中选择特定的属性放到另外一个集合中

bean的作用域

默认情况下,Spring应用上下文忠所有bean都是作为以单利的形式创建的。也就是说,无论注入多少次,每次所注入的都是同一个实例

但这样会造成对象被污染,所以需要考虑bean的作用域问题

Spring定义了多种作用域,可以基于这些作用域创建bean,包括:

  • 单例:在整个应用中,只创建bean的一个实例
  • 原型:每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例
  • 会话:为每个会话创建一个bean实例
  • 请求:为每个请求创建一个bean实例

为bean指定作用域,使用@Scope注解,其中需要注入两个参数,先写个模型

@Bean
@Scope {
    value = WebApplicationContext.SCOPE_SESSION,
            proxyMode = ScopedProxyMode.INTERFACES
}
public ShoppingCart cart(){}

先说value属性,他指定了bean的作用域

  • 单例:ConfigurableBeanFactory.SCOPE_SINGLETON
  • 原型:ConfigurableBeanFactory.SCOPE_PROTOTYPE
  • 会话:WebApplicationContext.SCOPE_SESSION
  • 请求:WebApplicationContext.SCOPE_REQUEST

然后再说说proxyMode属性,他解决了将会话或请求作用域的bean注入到单例bean中所遇到的问题。

因为Spring并不会将实际的会话、请求作用域bean注入到一个单例bean(以防对象污染),而是将他们的代理注入到这个单例bean,当单例调用这些bean的方法时,代理会对齐进行懒解析,并将调用委托给真实具体的bean.

proxyMode属性,指定了代理是基于接口的,还是基于类的

  • 基于接口:ScopedProxyMode.INTERFACES
  • 基于类:ScopedProxyMode.TARGET_CLASS

例如因为ShoppingCart是一个接口,所以使用proxyMode = ScopedProxyMode.INTERFACES表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。
但如果ShoppingCart是一个类,则需要使用proxyMode = ScopedProxyMode.TARGET_CLASS

文章作者: f1rry
文章链接: http://yoursite.com/2019/07/04/依赖注入DI/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 F1rry's blog