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()方法的参数有两个:ConditionContext和AnnotatedTypeMetadata,分别看下这两个接口的源码:
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 并非没有状态,如果它有自己的属性(变量),那么这些变量就会受到所有调用它的用户的影响,这是在实际应用中必须注意的。