Spring - 3. 注入

高级注入

环境注入

通过 profile, Spring 支持为不同的运行环境,如 dev, qa, prod 注入不同的 bean. 体现在:

  1. 为不同的环境配置不同的 configuration/xml
  2. 在同一个 configuration/xml 中, 为不同的环境配置不同的 bean

Java Config 配置 profile

为不同的环境配置不同的 configuration

	@Configuration
	@Profile("prod")
	public class ProductionProfileConfig {
	}

在同一个 configuration 中, 为不同的环境配置不同的 bean

	@Configuration
	public class DataSourceConfig {
		@Bean(destroyMethod="shutdown")
		@Profile("dev")
		public DataSource embeddedDataSource() {
			// ...
		}

		@Bean
		@Profile("prod")
		public DataSource jndiDataSource() {
			// ...
		}
	}

XML 配置 profile

为不同的环境配置不同的 xml

	<?xml version="1.0" encoding="UTF-8"?>
	<beans xmlns="http://www.springframework.org/schema/beans"
	  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	  xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	  xsi:schemaLocation="http://www.springframework.org/schema/jdbc
	    http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
	    http://www.springframework.org/schema/beans
	    http://www.springframework.org/schema/beans/spring-beans.xsd"
	   profile="dev">
	  <jdbc:embedded-database id="dataSource">
	    <jdbc:script location="classpath:schema.sql" />
	    <jdbc:script location="classpath:test-data.sql" />
	  </jdbc:embedded-database>
	</beans>

在同一个 xml 中, 为不同的环境配置不同的 bean

	<?xml version="1.0" encoding="UTF-8"?>
	<beans xmlns="http://www.springframework.org/schema/beans"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xmlns:jdbc="http://www.springframework.org/schema/jdbc"
		xmlns:jee="http://www.springframework.org/schema/jee"
		xmlns:p="http://www.springframework.org/schema/p"
		xsi:schemaLocation="
		http://www.springframework.org/schema/jee
		http://www.springframework.org/schema/jee/spring-jee.xsd
		http://www.springframework.org/schema/jdbc
		http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
		http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans.xsd">
	<beans profile="dev">
		<jdbc:embedded-database id="dataSource">
			<jdbc:script location="classpath:schema.sql" />
			<jdbc:script location="classpath:test-data.sql" />
		</jdbc:embedded-database>
	</beans>
	<beans profile="qa">
	<bean id="dataSource"
		class="org.apache.commons.dbcp.BasicDataSource"
		destroy-method="close"
		p:url="jdbc:h2:tcp://dbserver/~/test"
		p:driverClassName="org.h2.Driver"
		p:username="sa"
		p:password="password"
		p:initialSize="20"
		p:maxActive="30" />
	</beans>

激活 profile

Spring 有两个参数来指定哪个 profile 被激活:

  • spring.profiles.active, 若设置, 则 default 值无效
  • spring.profiles.default

若都没设, 则没有 profile 被激活.

profile 设置的方法有:

  1. DispatcherServlet 的初始化参数

  2. Web application 的 context param

  3. JNDI entries

  4. 环境变量

  5. JVM 系统属性

  6. @ActiveProfiles注解, 一般用于集成测试.

     @RunWith(SpringJUnit4ClassRunner.class)
     @ContextConfiguration(classes={PersistenceTestConfig.class})
     @ActiveProfiles("dev")
     	public class PersistenceTest {
     }
    

条件注入

条件注入适用于以下场景:

  1. classpath 中存在特定的库, 才会创建 bean
  2. 当 某一个 bean 被声明, 才会创建 bean
  3. 当 某环境变量被设置, 才会创建 bean

条件注入方法:

  1. 创建 Bean 时, 使用 @Conditional(条件类.class)

     @Bean
     @Conditional(MagicExistsCondition.class)
     public MagicBean magicBean() {
       return new MagicBean();
     }
    
  2. 创建 条件类, 该类需要实现 Condition 接口, 重写其 match 方法

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

ConditionContextAnnotatedTypeMetadata

通过 ConditionContext 可以获取的信息有:

    public interface ConditionContext {
      BeanDefinitionRegistry getRegistry(); // 检查 Bean Definition
      ConfigurableListableBeanFactory getBeanFactory(); // 获取 BeanFactory, 检查 Bean 的注入情况
      Environment getEnvironment(); // 检查环境变量
      ResourceLoader getResourceLoader(); // 获取资源
      ClassLoader getClassLoader(); // 获取 ClassLoader
    }

通过 AnnotatedTypeMetadata 可以获取的信息有:

    public interface AnnotatedTypeMetadata {
        boolean isAnnotated(String annotationType); // 是否被标注
        Map<String, Object> getAnnotationAttributes(String annotationType); // 获取标注的属性
        Map<String, Object> getAnnotationAttributes(String annotationType, boolean classValuesAsString); 
        MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType);
        MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType, boolean classValuesAsString);
    }

自动装配消除二义

如果在装配时有多个 Bean 满足条件, 那么会抛出:

org.springframework.beans.factory.NoUniqueBeanDefinitionException 异常

解决方法:

  1. @Primary, 提高一个 Bean 注入的优先级
  2. @Qualifier("BeanID"), 注入时选择要被注入的 Bean.
  3. 自定义 @Qualifier 标注

@Primary

  1. 可以作用在类名上, 修饰 @Component

         @Component
         @Primary
         public class IceCream implements Dessert { ... }
    
  2. 可以作用在方法上, 修饰 @Bean

         @Bean
         @Primary
         public Dessert iceCream() {
             return new IceCream();
         }
    
  3. 也可以使用 XML

          <bean id="iceCream class="com.desserteater.IceCream"
          primary="true" />
    

@Primary的不足在于, 同一种类型可以指定多个 @Primary, 此时, Spring 还是无法决定哪个 Bean 被注入, 会抛出异常.

@Qualifier

@Qualifier 可以和 @Autowired 或者 @Inject 一起连用, 可以指明要注入的 Bean.

        @Autowired
        @Qualifier("iceCream") // 注入 ID 或者 qualifier-name 为 iceCream 的 Bean
        public void setDessert(Dessert dessert) {
            this.dessert = dessert;
        }

@Qualifier("ID/QualifierName") 可以用 qualifier-name 来匹配. qualifier-name 可以通过以下方式指定:

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

或作用在方法上:

        @Bean
        @Qualifier("cold")
        public Dessert iceCream() {
            return new IceCream();
        }

使用 @Qualifier 的问题是:

  1. Bean 的 ID 是用字符串指定的, 而字符串不会被编译器检查, 相当于硬编码, 日后注入 Bean 的 Id 改动, 就会报错.
  2. 当两个 Bean 的 qualifier-name 相同时, 又会产生二义性.

自定义 @Qualifier 标注

使用自定义的 @Qualifer 标注可以解决:

  1. 首先定义一个注解

         @Target({ElementType.CONSTRUCTOR, ElementType.FIELD,
                  ElementType.METHOD, ElementType.TYPE})
         @Retention(RetentionPolicy.RUNTIME)
         @Qualifier
         public @interface Creamy { }
    
  2. 然后用注解修饰生成 Bean 的方法或者类

         @Component
         @Cold // 注解 Code
         @Creamy // 注解 Creamy
         public class IceCream implements Dessert { ... }
    
  3. 在注入的时候, 指定注解的 Bean

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

这样即解决了二义性的问题, 又解决了硬编码的问题.

Bean 的作用域

默认 Spring 的 Bean 为 Singleton. Spring 提供了以下作用域:

  • Singleton (默认)
  • Prototype
  • Session
  • Request

如何指定 Prototype 作用域?

  1. JavaConfig 中的 @Scope 注解可以指定 bean 的作用域:

         @Bean
         @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
         public Notepad notepad() {
           return new Notepad();
         }
    
  2. XML 的 scope 属性

         <bean id="notepad"
               class="com.myapp.Notepad"
               scope="prototype" />
    

Request 和 Session 作用域

为什么 Singleton 和 Prototype 不适用于 Web?

比如购物车, 不能所有人公用一个购物车(Singleton), 也不能系统每次需要就注入一个新的购物车.

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

与 Prototype 不同的是, Session 还要指定一个 proxyMode. 这是应为, 当服务启动 Spring 开始注入时, ShoppingCart 要注入到很多服务中:

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

但是此时这个对象还没有被创建. ShoppingCart 的创建是第一个相应用户请求到来时. 而且, Service 实际是要服务于多个用户, 意思是要注入不同的购物车.

所以, Spring 此时选择注入一个代理. 等到真正调用方法时, proxy 会将调用委托到真正的 ShoppingCart 方法上.

proxyMode 的选择

  • 如果 ShoppingCart 是接口, 那么 proxyMode 应该为 proxyMode=ScopedProxyMode.INTERFACES. JDK 实现的动态代理.
  • 如果 ShoppingCart 是具体类, 那么 proxyMode 应该未 proxyMode=ScopeProxyMode.TARGET_CLASS. CGLib 实现.

XML 方式注入 Session Bean

  1. 接口注入

         <bean id="cart"
               class="com.myapp.ShoppingCart"
               scope="session">
           <aop:scoped-proxy proxy-target-class="false" />
         </bean>
    
  2. 具体类注入

         <bean id="cart"
               class="com.myapp.ShoppingCart"
               scope="session">
           <aop:scoped-proxy />
         </bean>
    

运行时值注入

字符串直接在 JavaConfig 或者 XML 中注入属于 hardcode, 怎么解决呢?

  1. Property 占位符
  2. SpEL
posted @ 2017-02-04 13:35  still_water  阅读(337)  评论(0编辑  收藏  举报