Spring4详解系列(三)高级装配

1.环境与profile

不同的环境下,应用程序的配置项也不同,例如数据库配置、远程服务地址等。以数据库配置为例子,在开发环境中你可能使用一个嵌入式的内存数据库,并将测试数据放在一个脚本文件中。

例如,在一个Spring的配置类中,可能需要定义如下的bean:

@Configuration
public class Spring4ConfigTest {

    //@Bean(destroyMethod = "shutdown")
    /**
     * BasicDataSource提供了close()方法关闭数据源,所以必须设定destroy-method=”close”属性,
     * 以便Spring容器关闭时,数据源能够正常关闭;销毁方法调用close(),是将连接关闭,
     * 并不是真正的把资源销毁。
     */
    @Bean(destroyMethod = "close")
    public DataSource dataSource(){
        /**
         * 使用嵌入式数据源 EmbeddedDatabaseBuilder
         * 嵌入式数据源作为应用的一部分运行,非常适合在开发和测试环境中使用,
         * 但是不适合用于生产环境。因为在使用嵌入式数据源的情况下,
         * 你可以在每次应用启动或者每次运行单元测试之前初始化测试数据。
         */
        EmbeddedDatabaseBuilder embeddedDatabaseBuilder = new EmbeddedDatabaseBuilder();
        embeddedDatabaseBuilder.addScript("classpath:goods.sql")
                .addScript("classpath:order.sql");
        return embeddedDatabaseBuilder.build();
    }
    /**
     * 在生产环境下,你可能需要从容器中使用JNDI获取DataSource对象,
     * 这中情况下,对应的创建代码是:
     */
    @Bean
    public DataSource dataSourceJndi() {
        JndiObjectFactoryBean jndiObjectFactoryBean =
                new JndiObjectFactoryBean();
        jndiObjectFactoryBean.setJndiName("jdbc/myDS");
        jndiObjectFactoryBean.setResourceRef(true);
        jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
        return (DataSource) jndiObjectFactoryBean.getObject();
    }
    /**
     * 使用JNDI管理DataSource对象,很适合生产环境,但是对于日常开发环境来说太复杂了。
     * 另外,在QA环境下你也可以选择另外一种DataSource配置,可以选择使用普通的DBCP连接池,例如:
     */
    @Bean(destroyMethod = "close")
    public DataSource dataSourceDBCP() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setUrl("jdbc:h2:tcp://dbserver/~/test");
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        dataSource.setInitialSize(20);
        dataSource.setMaxActive(30);
        return dataSource;
    }

配置profile beans

Spring提供的方法不是在构件时针对不同的环境决策,而是在运行时,这样,一个应用只需要构建一次,就可以在开发、QA和生产环境运行。

在Spring 3.1之中,可以使用@Profile注解来修饰JavaConfig类,当某个环境对应的profile被激活时,就使用对应环境下的配置类。

在Spring3.2之后,则可以在函数级别使用@Profile注解(是的,跟@Bean注解同时作用在函数上),这样就可以将各个环境的下的bean定义都放在同一个配置类中,还是以之前的例子:

@Configuration
// @Profile("dev") 环境也可以配置在类上
public class Spring4ConfigTest {
    @Bean(destroyMethod = "close")
    @Profile("dev")
    public DataSource dataSource(){
        EmbeddedDatabaseBuilder embeddedDatabaseBuilder = new EmbeddedDatabaseBuilder();
        embeddedDatabaseBuilder.addScript("classpath:goods.sql")
                .addScript("classpath:order.sql");
        return embeddedDatabaseBuilder.build();
    }
    @Bean
    @Profile("prod")
    public DataSource dataSourceJndi() {
        JndiObjectFactoryBean jndiObjectFactoryBean =
                new JndiObjectFactoryBean();
        jndiObjectFactoryBean.setJndiName("jdbc/myDS");
        jndiObjectFactoryBean.setResourceRef(true);
        jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
        return (DataSource) jndiObjectFactoryBean.getObject();
    }
}

激活profiles

Spring提供了spring.profiles.active和spring.profiles.default这两个配置项定义激活哪个profile。如果应用中设置了spring.profiles.active选项,则Spring根据该配置项的值激活对应的profile,如果没有设置spring.profiles.active,则Spring会再查看spring.profiles.default这个配置项的值,如果这两个变量都没有设置,则Spring只会创建没有被profile修饰的bean。 

测试环境时,可使用如下方式配置:

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("dev") // 配置测试环境
public class DemoApplicationTests {}

2.条件化的Bean

判断Bean的加载环境是否存在,如果存在,则创建Bean

@Configuration
public class Spring4ConfigTest {
    @Bean
    // 条件化的创建Bean
    @Conditional(Spring4ServiceTest.class)
    public User gainUser(){
        System.out.println("如果存在配置的‘bock’环境,将创建该Bean");
        return new User();
    }
}

// 创建Spring4ServiceTest类,并实现Condition接口
@Service
public class Spring4ServiceTest implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 如果返回false,则不创建ID为gainUser的bean,为true则创建
        Environment environment = context.getEnvironment();
        boolean bock = environment.containsProperty("bock");
        return bock;
    }
}

述代码中的matchs()方法简单且有效:它首先获取Environment变量,然后再判断环境变量中是否存在bock属性。在这个例子中,bock的值是多少并不重要,它只要存在就好。

Spring4ServiceTest matchs()方法是通过ConditionContext获取了Environment实例。matchs()方法的参数有两个:ConditionContextAnnotatedTypeMetadata,分别看下这两个接口的源码:

ConditionContext:

public interface ConditionContext {
    BeanDefinitionRegistry getRegistry();
    ConfigurableListableBeanFactory getBeanFactory();
    Environment getEnvironment();
    ResourceLoader getResourceLoader();
    ClassLoader getClassLoader();
}
  • 借助getRegistry()方法返回的BeanDefinitionRegistry实例,可以检查bean的定义;
  • 借助getBeanFactory()方法返回的ConfigurableListableBeanFactory实例,可以检查某个bean是否存在于应用上下文中,还可以获得该bean的属性;
  • 借助getEnvironment()方法返回的Environment实例,可以检查指定环境变量是否被设置,还可以获得该环境变量的值;
  • 借助getResourceLoader()方法返回的ResourceLoader实例,可以得到应用加载的资源包含的内容;
  • 借助通过getClassLoader()方法返回的ClassLoader实例,可以检查某个类是否存在。

AnnotatedTypeMetadata接口则能够让我们检查带有@Bean注解的方法上还有什么其他的注解

package org.springframework.core.type;

import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;

public interface AnnotatedTypeMetadata {
    boolean isAnnotated(String var1);
    Map<String, Object> getAnnotationAttributes(String var1);
    Map<String, Object> getAnnotationAttributes(String var1, boolean var2);
    MultiValueMap<String, Object> getAllAnnotationAttributes(String var1);
    MultiValueMap<String, Object> getAllAnnotationAttributes(String var1, boolean var2);
}
  • 借助isAnnotated()方法,能够判断带有@Bean注解的方法是不是还有其他特定的注解。
  • 借助其他方法能检查@Bean注解的方法上其他注解的属性

从Spring 4开始,@Profile注解也利用@Conditional注解和Condition接口进行了重构。作为分析@Conditional注解和Condition接口的另一个例子,我们可以看下在Spring 4中@Profile注解的实现。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional({ProfileCondition.class})
public @interface Profile {
    String[] value();
}

可以看出,@Profile注解的实现被@Conditional注解修饰,并且依赖于ProfileCondition类——该类是Condition接口的实现。如下列代码所示,ProfileCondition利用ConditionContext和AnnotatedTypeMetadata两个接口提供的方法进行决策

class ProfileCondition implements Condition {
    ProfileCondition() {}
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if(context.getEnvironment() != null) {
            MultiValueMap attrs = 
                         metadata.getAllAnnotationAttributes(Profile.class.getName());
            if(attrs != null) {
                Iterator var4 = ((List)attrs.get("value")).iterator();
                Object value;
                do {
                    if(!var4.hasNext()) {
                        return false;
                    }
                    value = var4.next();
                } while(!context.getEnvironment().acceptsProfiles((String[]) 
                               ((String[])value)));
                return true;//传给@Profile注解的参数对应的环境profiles已激活
            }
        }
        return true; //默认为true
    }
}

可以看出,ProfileCondition通过AnnotatedTypeMetadata实例获取与@Profile注解相关的所有注解属性; 然后检查每个属性的值(存放在value实例中),对应的profiles别激活——即context.getEnvironment().acceptsProfiles(((String[]) value))的返回值是true,则matchs()方法返回true。 Environment类提供了可以检查profiles的相关方法,用于检查哪个profile被激活: String[] getActiveProfiles() -- 返回被激活的profiles数组;  String[] getDefaultProfiles() -- 返回默认的profiles数组;boolean acceptsProfiles(String... profiles) -- 如果某个profiles被激活,则返回true。

3.处理自动装配的歧义性

创建一个SpringBoot项目,一个ColorService接口,两个RedColor和BlueColor类的实现:

// ColorService 接口
public interface ColorService { public void colorType();}

// BlueColor 实现类
@Primary
@Component
public class BlueColor implements ColorService{
	@Override
	public void colorType() {
		System.out.println("我的颜色是蓝色...");
	}
}

// RedColor 实现类
@Component
public class RedColor implements ColorService{
	@Override
	public void colorType() {
		System.out.println("我的颜色是红色...");
	}
}

// 启动类
@SpringBootApplication
public class Spring4DemoApplication {
	
	private ColorService color;

	public static void main(String[] args) {
		SpringApplication.run(Spring4DemoApplication.class, args);
	}
	
	/**
	 * 如果ColorService的两个接口实现类上,有一个类上有@Primary注解时,
	 * 使用colorPrimary的方式注入,局限性:只能标示一个优先可选方案
	 */
//	@Autowired
//	public void colorPrimary(ColorService colorService) {
//		this.color = colorService;
//	}
	
	/**
	 * 如果ColorService的两个接口实现类上,没有有@Primary注解时,
	 * 使用colorQualifier的方式注入,使用@Qualifier("类的名字")指定要注入的组件
	 * 也可以在组件上自定义名称,如在组件BlueColor上命名@Qualifier("bc"),即bc限定符分配给了BlueColor
	 * 那么,在colorQualifier上引用时,也应该是对应的@Qualifier("bc")
	 */
	@Autowired
	@Qualifier("blueColor")
	public void colorQualifier(ColorService colorService) {
		this.color = colorService;
	}
	
	@Bean
	public void method() {
		color.colorType();
	}
}

4.Bean的作用域

默认情况下,Spring应用上下文中的bean都是单例对象,也就是说,无论给某个bean被多少次装配给其他bean,都是指同一个实例。

但有时候,你可能会发现,所使用的类是易变的(mutable),他们会保持一些状态,因此重用是不安全的。在这种情况下,将class声明为单例的Bean就不是什么好主意了,因为对象会被污染,然后重用被污染的对象时会出现意想不到的问题。

大部分情况下,单例bean很好用:如果一个对象没有状态并且可以在应用中重复使用,那么针对该对象的初始化和内存管理开销非常小。 但是,有些情况下你必须使用某中可变对象来维护几种不同的状态,因此形成非线程安全。

在这种情况下,把类定义为单例并不是一个好主意——该对象在重入使用的时候可能遇到线程安全问题。

Spring定义了几种bean的作用域,列举如下:

【单例】Singleton——在整个应用中只有一个bean的实例;

【原型】Prototype——每次某个bean被装配给其他bean时,都会创建一个新的实例;

【会话】Session——在web应用中,在每次会话过程中只创建一个bean的实例;

【请求】Request——在web应用中,在每次http请求中创建一个bean的实例。

Singleton域是默认的作用域,

如前所述,对于可变类型来说并不理想。我们可以使用@Scope注解——和@Component或@Bean注解都可以使用

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad{ ... }

定义prototype类型的bean,每次Notepad被装配到其他bean时,都会重新创建一个新的实例。

使用会话和请求作用域

在Web应用中,如果能够实例化在会话和请求范围内共享的bean,那将是非常有价值的事情。例如,在典型的电子商务应用中,可能会有一个bean代表用户的购物车。如果购物车是单例的话,那么将会导致所有的用户都会向同一个购物车中添加商品。另一方面,如果购物车是原型作用域的,那么在应用中某一个地方往购物车中添加商品,在应用的另外一个地方可能就不可用了,因为在这里注入的是另外一个原型作用域的购物车。
就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。要指定会话作用域,我们可以使用@Scope注解,它的使用方式与指定原型作用域是相同的:

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

在这里你通过value属性设置了WebApplicationContext.SCOPE_SESSION,这告诉Spring为web应用中的每个session创建一个ShoppingCartBean的实例。在整个应用中会有多个ShoppingCart实例,但是在某个会话的作用域中ShoppingCart是单例的。 这里还用proxyMode属性设置了ScopedProxyMode.INTERFACES值,这个属性解决了把request/session scope的bean装配到singleton scope的bean时会遇到。首先看下这个问题的表现。 假设在应用中需要将ShoppingCartbean装配给单例StoreServicebean的setter方法:

@Component
public class StoreService {
    @Autowired
    public void setShoppingCart(ShoppingCart shoppingCart) {
        this.shoppingCart = shoppingCart;
    }
}

因为StoreService是一个单例的bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShoppingCart bean注入到setShoppingCart()方法中。但是ShoppingCart bean是会话作用域的,此时并不存在。直到某个用户进入系统,创建了会话之后,才会出现ShoppingCart实例。
另外,系统中将会有多个ShoppingCart实例:每个用户一个。我们并不想让Spring注入某个固定的ShoppingCart实例到StoreService中。我们希望的是当StoreService处理购物车功能时,它所使用的ShoppingCart实例恰好是当前会话所对应的那一个。Spring并不会将实际的ShoppingCart bean注入到StoreService中,Spring会注入一个到ShoppingCart bean的代理,如图3.1所示。这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。
现在,我们带着对这个作用域的理解,讨论一下proxyMode属性。如配置所示,proxyMode属性被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。
如果ShoppingCart是接口而不是类的话,这是可以的(也是最为理想的代理模式)。但如果ShoppingCart是一个具体的类的话,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须
要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。
尽管我主要关注了会话作用域,但是请求作用域的bean会面临相同的装配问题。因此,请求作用域的bean应该也以作用域代理的方式进行注入。

1.有状态会话bean :每个用户有自己特有的一个实例,在用户的生存期内,bean保持了用户的信息,即“有状态”;一旦用户灭亡(调用结束或实例结束),bean的生命期也告结束。即每个用户最初都会得到一个初始的bean。

2.无状态会话bean :bean一旦实例化就被加进会话池中,各个用户都可以共用。即使用户已经消亡,bean 的生命期也不一定结束,它可能依然存在于会话池中,供其他用户调用。由于没有特定的用户,那么也就不能保持某一用户的状态,所以叫无状态bean。但无状态会话bean 并非没有状态,如果它有自己的属性(变量),那么这些变量就会受到所有调用它的用户的影响,这是在实际应用中必须注意的。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值