Spring系列(三) Bean装配的高级技术

发布日期:2019-03-11

profile

不同于maven的profile spring的profile不需要重新打包 同一个版本的包文件可以部署在不同环境的服务器上 只需要激活对应的profile就可以切换到对应的环境.

@Profile({"test""dev"}) Java Config 通过这个注解指定bean属于哪个或哪些profile. 参数value是一个profile的字符串数组. 此注解可以添加到类或方法上. XML Config 对应的节点是beans的属性profile="dev" 可以在根<beans>节点下嵌套定义分属不同profile的节点 形成如下结构

<beans ...> <beans profile="dev"> <bean ...> <bean ...> </beans> <beans profile="prod"> <bean ...> </beans> ...</beans>通过spring.profiles.activespring.profiles.default可以设置激活哪个profile如果是多个就用""分开 spring优先使用spring.profiles.active的设置 如果找不到 就使用spring.profiles.default设置的值 如果二者都没设置 spring会认为没有要激活的profile 它只会创建不加@Profile的那些bean.

可以通过多种方式设置这两个属性:

    DispacherServlet的初始化参数Web应用的上下文参数JNDI环境变量JVM系统属性测试类上使用@ActiveProfiles注解

下面是web.xml中在上下文以及在servlet中设置spring.profiles.default的代码

<web-app> <!--上下文设置default profile--> <context-param> <param-name>spring.profiles.default</param-name> <param-value>dev</param-value> </context-param> <servlet> ... <!--设置default profile--> <init-param> <param-name>spring.profiles.default</param-name> <param-value>dev</param-value> </init-param> </servlet></web-app>

条件化的 bean

Spring4.0 引入了条件化bean 这些bean只有在满足一定条件下才会创建. profile就是条件化的一种使用方式 事实上4.0版本后的profile实现机制与其他条件化bean完全一样 我们可以通过profile的源码一窥条件化bean的机制.

@Target({ElementType.TYPE ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Conditional(ProfileCondition.class) //注意这里 条件化bean的注解public @interface Profile { String[] value()}@Conditional(ConditionClass.class) Java Config 可以将这个注解添加到@Bean注解的方法上 参数value是一个Class类型的变量 这个类必须实现Condition接口 方法matchs()返回true则加载bean 否则忽略.

Condition 接口定义如下:

@FunctionalInterfacepublic interface Condition { boolean matches(ConditionContext context AnnotatedTypeMetadata metadata) }

ProfileCondition的定义

class ProfileCondition implements Condition { @Override public boolean matches(ConditionContext context AnnotatedTypeMetadata metadata) { // 获取profile注解的属性 MultiValueMap<String Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()) if (attrs != null) { // 获取value属性的值 for (Object value : attrs.get("value")) { // 测试profile是否激活 if (context.getEnvironment().acceptsProfiles((String[]) value)) { return true } } return false } return true }}通过matchs()方法的两个参数我们可以做到 (1)通过ConditionContext获取到上下文所需信息(2)通过AnnotatedTypeMetadata获取到bean的注解

public interface ConditionContext { // 获取bean的注册信息 从而可以检查bean定义 BeanDefinitionRegistry getRegistry() // 获取bean工厂从而可以判断bean是否存在获取其他bean获取bean的状态信息 @Nullable ConfigurableListableBeanFactory getBeanFactory() // 获取环境变量profile中正是使用此对象的方法acceptsProfiles()检查profile是否激活 Environment getEnvironment() // 获取加载的资源 ResourceLoader getResourceLoader() // 获取classLoader @Nullable ClassLoader getClassLoader()}public interface AnnotatedTypeMetadata { // 检查是否由某注解 boolean isAnnotated(String annotationName) // 下面几个方法可以获取bean的注解包括其属性 @Nullable Map<String Object> getAnnotationAttributes(String annotationName) @Nullable Map<String Object> getAnnotationAttributes(String annotationName boolean classValuesAsString) @Nullable MultiValueMap<String Object> getAllAnnotationAttributes(String annotationName) @Nullable MultiValueMap<String Object> getAllAnnotationAttributes(String annotationName boolean classValuesAsString)}

自动装配的歧义

自动装配大大简化了spring的配置 对于大多数应用对象的依赖bean 程序实现的时候一般只有一个匹配 但也存在匹配到多个bean的情况 Spring处理不了这种情况 这时候就需要由开发人员为其消除装配的歧义.

@Primary 注解用来指定被注解的bean为首选bean. 但如果多个匹配的bean都添加了该注解依然无法消除歧义.@Qualifier 注解用来限定bean的范围. 与@AutoWired@Inject共同使用时参数value用来指定beanid 但使用默认的beanid对重构不友好. 与@Component@Bean一起使用时参数value为bean指定别名 别名一般是带有描述bean特征的词描述 然后在注入时就可以使用别名. 但别名可能需要定义多个 Qualifier不支持数组 也不允许定义多个(原因见下面).@Qualifier 不允许对同一bean重复标记 这是因为Qualifier注解定义没有添加@Repeatable注解.

可以使用自定义限定注解的方式达到缩小bean范围的目的.

下面代码使用Qualifier的方式定义bean

@Component@Qualifier("cheap")public class QQCar implements Car{}@Component@Qualifier("fast")public class BenzCar implements Car{ }@Component@Qualifier("safe")//@Qualifier("comfortable") //行不通不能加两个Qualifierpublic class BMWCar implements Car{ }

再使用自定义限定注解的方式

// 定义...@Qualifierpublic @interface Cheap{}...@Qualifierpublic @interface Fast{}...@Qualifierpublic @interface Safe{}...@Qualifierpublic @interface Comfortable{}// 使用@Component@Safe@Comfortablepublic class BMWCar implements Car{ }...

使用自定义限定的方式可以随意组合来限定bean的范围 因为没有任何使用string类型 所以也是类型安全的.

bean 作用域

bean的四种作用域:

    单例(Singleton) 默认为此作用域 全局唯一原型(Prototype) 注入或从上下文获取时均会创建会话(Session) Web程序使用 同一会话保持唯一请求(Request) Web程序使用每次请求唯一
@Scope Java Config 用此注解指定bean的作用域 参数value用来指定作用域 如value=ConfigurableBeanFactory.SCOPE_PROTOTYPE 原型和单例的常数定义在ConfigurableBeanFactory中 会话和请求的常数定义在WebApplicationContext中 另一个参数proxyMode=ScopedProxyMode.INTERFACES用来指定创建代理的方式 示例中指定了使用基于接口的代理方法 如果使用proxyMode=ScopedProxyMode.TARGET_CLASS则会使用CGLib来生成基于类的代理.XML Config对应的时bean的scope="prototype"属性 针对web作用域还需要指定使用代理示例代码如下.

<bean id="beanid" scope="session"> <!--需要引用aop命名空间的. proxy-target-class 默认为true 使用CGLib创建代理 指定为false则使用接口方式创建代理--> <aop:scoped-proxy proxy-target-></bean>作用域为什么使用代理模式? 如果bean不是单例的 spring会为其创建一个代理对象 再将这个代理对象注入进去 实际运行过程中由代理对象委托调用实际的bean. 这样有两点好处:(1)懒加载 bean可以在需要时才创建(2)便于作用域扩展

属性占位符和SpEL

spring提供了两种运行时注入值的方式 这样就避免了将字面量硬编码到程序或配置文件里面.

    属性占位符SpEL

一. 属性占位符

先看下面的例子:

@Configuration@PropertySource("classpath:/path/app.property")public class MyConfig{ @AutoWired Environment env @Bean public Car getCar(){ return new QQCar(env.getProperty("qqcar.price")) }}@PropertySource("classpath:/path/app.property") 可以指定加载资源文件的位置 结合使用Environment类的实例env可以获取到资源文件中配置的节点.Environment不仅可以获取到配置的字面量 它也可以获取复杂类型 将其转化为对应Class的实例.<T> T getProperty(String key Class<T> targetType)XML Congif 可以使用占位符 形如${qqcar.price} 前提是配置了context命名空间下的<context:property-placeholder /> 它会生成类型为PropertySourcesPlaceholderConfigurer的bean 该bean用来解析占位符如果开启组件扫描和自动装配的话 就没有必要指定资源文件或类了 可以使用@Value(qqcar.price). 同样 需要一个类型为PropertySourcesPlaceholderConfigurer的bean.

@Beanpublic static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(){ return new PropertySourcesPlaceholderConfigurer()}

二. 功能更强大的SpEL

SpEL的特性如下:

    使用bean的Id来引用bean调用方法和属性: 可以通过beanid来调用bean的方法和属性 就像在代码中一样.对值进行算术逻辑关系运算: 特别注意: ^乘方运算符 ?:为空运算符正则表达式匹配: 形如#{beanid.mobile matches "1(?:3d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8d|9d)d{8}"}集合操作: .?[]查找运算符 .^[]查找匹配的第一项.$[]查找匹配的最后一项.![]投影运算符(比如获取集合中符合条件的对象 并取其中某个属性映射到结果集合中)

SpEL的示例 #{1}#{T(System).out.print("test")}#{beanId.name}#{systemProperies["path"]}

虽然SpEL功能强大 我们可以通过SpEL编写复杂的表达式 但过分复杂的表达式不适合理解和阅读 同时也会增大测试的难度. 因此建议尽量编写简洁的表达式.

1 0 9)