条件化 bean

有时候我们要满足某种情况才将bean 初始化放入容器中。

基于环境初始化不同的 bean

1.申明接口并创建两个实现类

public interface Teacher {

    void startWorking();
}

public class JavaTeacher implements Teacher {

    public void startWorking() {
        System.out.println("开始 Java 教学");
    }
}

public class SqlTeacher implements Teacher {

    public void startWorking() {
        System.out.println("开始 SQL 教学");
    }
}
public interface Teacher {

    void startWorking();
}

public class JavaTeacher implements Teacher {

    public void startWorking() {
        System.out.println("开始 Java 教学");
    }
}

public class SqlTeacher implements Teacher {

    public void startWorking() {
        System.out.println("开始 SQL 教学");
    }
}

2.JavaConfig 显式装配两个实现类

@Configuration
public class ApplicationConfig {


    @Bean(name = "teacher")
    @Profile("Monday")
    public Teacher sqlTeacher() {
        return new SqlTeacher();
    }


    @Bean(name = "teacher")
    @Profile("Tuesday")
    public Teacher javaTeacher() {
        return new JavaTeacher();
    }

}
@Configuration
public class ApplicationConfig {


    @Bean(name = "teacher")
    @Profile("Monday")
    public Teacher sqlTeacher() {
        return new SqlTeacher();
    }


    @Bean(name = "teacher")
    @Profile("Tuesday")
    public Teacher javaTeacher() {
        return new JavaTeacher();
    }

}

注:可以看到两个 bean 都取名为 teacher,但是 @Profile 值不同。

3.单元测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ApplicationConfig.class)
@ActiveProfiles("Monday")
//@ActiveProfiles("Tuesday")
public class ConditionTest {
    @Autowired(required =  false)
    private Teacher teacher;

    @Test
    public void test01(){
        if(teacher != null){
            teacher.startWorking();
        }
    }
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ApplicationConfig.class)
@ActiveProfiles("Monday")
//@ActiveProfiles("Tuesday")
public class ConditionTest {
    @Autowired(required =  false)
    private Teacher teacher;

    @Test
    public void test01(){
        if(teacher != null){
            teacher.startWorking();
        }
    }
}

执行单元测试,查看控制台输出:

开始 SQL 教学
开始 SQL 教学

通过 @ActiveProfiles 配置当前的环境,我们就能做到 周一 SQL教学 周二 Java 教学 了。

激活 Profile 方式

Spring 激活 Profile 依赖两个属性:

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

active 优先级比 default 高,如果两个属性都没有设置的话,将只会创建没有定义 profilebean

除了 @ActiveProfiles 配置外,我们还能通过很多种方式来设置属性:

  • 作为 DispatcherServlet 的初始化参数;
  • 作为 Web 应用的上下文参数;
  • 作为 JNDI 条目;
  • 作为环境变量;
  • 作为 JVM 的系统属性;

自定义提交

除了环境之外,我们也可以自定义条件来决定初始化那个 Bean。

例如刚刚的实例,老师开始 Java教学 ,除了时间是周二之外还应该有学生在,才能完成教学。

@Configuration
public class ApplicationConfig {

    @Bean(name = "teacher")
    @Conditional(MyCondition.class)
    public Teacher javaTeacher() {
        return new JavaTeacher();
    }
}
@Configuration
public class ApplicationConfig {

    @Bean(name = "teacher")
    @Conditional(MyCondition.class)
    public Teacher javaTeacher() {
        return new JavaTeacher();
    }
}

通过 @Conditional 注解来自定义条件,MyCondition 是我们实现 Condition 接口完成的自定义条件判断

public class MyCondition implements Condition {

    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        Environment environment = conditionContext.getEnvironment();
        // 1.获取属性值
        List activeProfiles = Arrays.asList(environment.getActiveProfiles());
        List defaultProfiles = Arrays.asList(environment.getDefaultProfiles());
        // 2.判断属性是否存在
        boolean activeFlag = activeProfiles.contains("Tuesday") && activeProfiles.contains("students");
        boolean defaultFlag = defaultProfiles.contains("Tuesday") && defaultProfiles.contains("students");
        return activeFlag || defaultFlag;
    }
}
public class MyCondition implements Condition {

    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        Environment environment = conditionContext.getEnvironment();
        // 1.获取属性值
        List activeProfiles = Arrays.asList(environment.getActiveProfiles());
        List defaultProfiles = Arrays.asList(environment.getDefaultProfiles());
        // 2.判断属性是否存在
        boolean activeFlag = activeProfiles.contains("Tuesday") && activeProfiles.contains("students");
        boolean defaultFlag = defaultProfiles.contains("Tuesday") && defaultProfiles.contains("students");
        return activeFlag || defaultFlag;
    }
}

单元测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ApplicationConfig.class)
@ActiveProfiles({"Tuesday","students"})
public class ConditionTest {
    @Autowired(required =  false)
    private Teacher teacher;

    @Test
    public void test01(){
        if(teacher!=null){
            teacher.startWorking();
        }
    }
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ApplicationConfig.class)
@ActiveProfiles({"Tuesday","students"})
public class ConditionTest {
    @Autowired(required =  false)
    private Teacher teacher;

    @Test
    public void test01(){
        if(teacher!=null){
            teacher.startWorking();
        }
    }
}

控制台输出:

开始 Java 教学
开始 Java 教学
ConditionContext 方法
public interface ConditionContext {

    BeanDefinitionRegistry getRegistry();

    ConfigurableListableBeanFactory getBeanFactory();

    Environment getEnvironment();

    ResourceLoader getResourceLoader();

    ClassLoader getClassLoader();
}
public interface ConditionContext {

    BeanDefinitionRegistry getRegistry();

    ConfigurableListableBeanFactory getBeanFactory();

    Environment getEnvironment();

    ResourceLoader getResourceLoader();

    ClassLoader getClassLoader();
}
• getRegistry():返回 BeanDefinitionRegistry 检查 bean 定义;
• getBeanFactory():返回 ConfigurableListableBeanFactory 检查 bean 是否存在和它的属性;
• getEnvironment():返回 Environment 检查环境变量是否存在以及它的值;
• getResourceLoader():返回 ResourceLoader 所加载的资源;
• getClassLoader():返回 ClassLoader 加载并检查类是否存在

处理自动装配歧义性

在之前我们讲到自动装配如果匹配到多个 bean 的话,Spring 会抛出一个 NoUniqueBeanDefinitionException 异常,表示没有明确指明使用哪个 bean 来进行自动装配。

标识首选 bean

通过 @Primart 注解可以将一个 bean 设置为首选,但是要注意的是,如果出现多个 @Primart 也还是会抛出异常。

限定自动装配的 bean

@Primart 注解不能将可选范围缩小到唯一一个无歧义的选项中。我们可以通过限定符的形式进行范围的缩小,直到唯一为止。

@Qualifier 注解是使用限定符的主要方式。它可以与 @Autowired 注解协同使用,在注入 bean 的时候指定注入的是那个 bean

public interface Teacher {

    void startWorking();
}

@Component
public class SqlTeacher implements Teacher {

    public void startWorking() {
        System.out.println("开始 SQL 教学");
    }
}

@Component
public class JavaTeacher implements Teacher {

    public void startWorking() {
        System.out.println("开始 Java 教学");
    }
}
public interface Teacher {

    void startWorking();
}

@Component
public class SqlTeacher implements Teacher {

    public void startWorking() {
        System.out.println("开始 SQL 教学");
    }
}

@Component
public class JavaTeacher implements Teacher {

    public void startWorking() {
        System.out.println("开始 Java 教学");
    }
}
@Autowired
@Qualifier("javaTeacher")
private Teacher teacher;

@Test
public void test01() {
    teacher.startWorking();
}
@Autowired
@Qualifier("javaTeacher")
private Teacher teacher;

@Test
public void test01() {
    teacher.startWorking();
}

@Qualifier 注解的参数就是想要注入的 beanid。如果 @Component 没有显示指定 bean 的 Id ,那么默认的 Id 是类型首字母小写。这跟类名就有耦合性了,我们可以在类上加 @Qualifier(name) 的形式设置别名避免和类名耦合。

自定义限定符

要注意的是 @Qualifier 也可能出现重复,我们可以通过增加多个条件来进一步缩小范围,但是 Java 不支持在同一个条目上出现相同类型的多个注解,我们可以通过自定义注解的形式来解决这个问题。

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD,
        ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Monday {

}
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD,
        ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Monday {

}

我们定义了一个 Monday 注解,我们来使用一下它:

@Component
@Monday
public class JavaTeacher implements Teacher {

    public void startWorking() {
        System.out.println("开始 Java 教学");
    }
}
@Component
@Monday
public class JavaTeacher implements Teacher {

    public void startWorking() {
        System.out.println("开始 Java 教学");
    }
}

使用:

@Autowired
@Monday
private Teacher teacher;
@Autowired
@Monday
private Teacher teacher;

bean 作用域

默认情况下 Spring 应用上下文中的所有 bean 都是以单例形式注入,但是在某些情况下可能就不太适用。比如购物车组件这种有状态值的,针对每一个用户应该是不同的实例。

Spring 定义了多种作用域:

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

如何指定作用域

通过使用 @Scope 注解或 scope标签 可以配置作用域。

运行时注入属性

Spring 提供了两种在运行时赋值的方式:

  • 属性占位符(Property plaveholder)
  • Spring 表达式语言(SpEL)

它们都可以在应用运行时在获取值,避免硬编码的存在。