Spring之高级装配
一、环境与Profile(spring 4.0+)
配置profile bean,在实际生产中往往需要几套环境,如dev、test和prod等,它们的配置(如数据源)往往不同,这就需要在不同的环境生成不同的bean,spring中用@Profile注解以实现该功能。
1、@Profile("profile"):指定某个或某些bean属于指定的profile下,当该profile激活(Active)时,属于这个profile下的bean会被创建。@Profile可用在class级别和@Bean标注的方法级别上,在class级别上的profile激活的话,则该类中所有的bean均会被创建,同样在方法级别上只有那一个bean会被创建,相当于进行细粒度化管理。注意哪些不指定profile的bean无论何时都会创建,@Profile可传入多个profile,它可以在不同配置下创建同一个bean,比如@Profile({"dev", "test"})表示在dev或test环境下均创建同一个bean,如启用同一个数据源。
//示例代码 @Configuration public class Config { @Profile("dev") public String devStr(){ return "dev"; } @Profile("prod") public String prodStr(){ return "prod"; } }
2、XML配置profile,在xml中可以在<beans>的profile属性上配置profile 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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <beans profile="dev"> <bean id="xxx-dev" class="com.xxx.xxx"> </bean> </beans> <beans profile="prod"> <bean id="xxx-prod" class="com.xxx.xxx"> </bean> </beans> </beans>
3、激活profile:主要依赖spring.profiles.active和spring.profiles.default,前者优先级高,active未设置就启用default,如果两者都没设置就只创建没有在任何profile中的bean。常用激活方式如下:
- 作为DispatcherServlet的初始化参数,web.xml节选:
<!--省略其他声明和配置--> <web-app> <context-param> <param-name>spring.profiles.default</param-name> <param-value>dev</param-value> </context-param> <servlet> <init-param> <param-name>spring.profile.active</param-name> <param-value>prod</param-value> </init-param> </servlet> </web-app>
- 作为web应用的上下文参数;(未尝试)
- 作为JNDI条目;(未尝试)
- 作为环境变量;(未尝试)
- 作为JVM的系统属性,添加 -Dspring.profiles.active=test 参数
- 在测试类中,使用@ActiveProfile注解设置:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = Config.class) @ActiveProfiles({"prod", "dev"}) public class ProfileTest { //Other code }
如上面Java代码所示,也可以同时激活多个profile,一般是不同维度的多个profile。
二、条件化的bean
在Java中经常会遇到if满足某个条件,则创建某个对象,在spring中同样存在此问题,即满足某些条件才创建一些bean。在Spring 4中引入@Conditional注解进行条件化配置bean,条件满足则创建bean,不满足则忽略。
1、@Conditional(ConditionClass.class):它可以用在配置类上和@Bean注解的方法上,与@Profile类似,只是作用范围不同。它的参数是一个实现了Condition接口的类,这个类只有一个返回值为boolean类型的matches方法,根据该方法的返回值确定是否创建bean,示例如下:
//实现了Condition接口的任意类 public class MagicExistsCondition implements Condition { public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { Environment evn = conditionContext.getEnvironment(); return evn.containsProperty("magic"); } } //@Conditional注解的使用 @Configuration @PropertySource("classpath:app.properties")//向环境中增加magic属性 public class MagicConfig { @Autowired Environment evn; @Bean @Conditional(MagicExistsCondition.class) public String getMagicBean() { return evn.getProperty("magic", "can not get property of magic"); } }
可以看到matches方法有两个参数,ConditionContext 和 AnnotatedTypeMetadata,这两个参数为我们做决策提供了大量的信息,具体含义不在此处详解。其实@Profile也是利用@Conditional去决定创建那些bean,其源码如下:
//@Profile的定义 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional({ProfileCondition.class})//这里利用@Conditional确定是否创建相应的bean public @interface Profile { String[] value(); } //ProfileCondition类的实现 class ProfileCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null) { for (Object value : attrs.get("value")) { if (context.getEnvironment().acceptsProfiles((String[]) value)) { return true; } } return false; } return true; } } //这个类实际上就是扫描那些带有@Profile的方法和类,然后再遍历@Profile中的参数, //如果该参数在environment中存在,就返回true创建bean,否则返回false忽略。
三、处理自动装配有歧义的bean
如果我们同时出现同一个类型(一个接口的多种实现,或一个抽象类的多种继承)的多个bean,这时自动装配将无法知道该装配谁合适,因此Spring提供了@Primary和@Qualifier的注解帮我们解决这个问题。
1、@Primary:这个注解表示多个同类型的bean中,带该注解的bean是最主要的,装配时优先选择,它同样可以用于class和@Bean注解的method上面,只是作用范围不一样。正如你所想的那样,如果出现多个同一类型的bean且都带有@Primary注解(如@Primary加在class上,而class里面能创建多个同一类型的bean),此时spring依然无法做出选择,这是另外一个注解@Qualifier出现了。
2、@Qualifier("qualifier-id"):这个注解可以用在自动装配(@Autowire)处,也可用在创建bean(@Bean&@Component...)的地方,和@Autowire连用表示选择限定符为qualifier-id的bean进行装配,和声明为bean的注解(@Bean&@Component...)一起使用表示为这个bean设置限定符。注意此处的“限定符”不是bean-id,如果没有显示指定限定符,默认和bean-id一致,在日常使用中经常在@Autowire处的@Qualifier中直接写bean-id就可以,如果连bean-id都没指定的话,bean-id默认就是首字母小写的类名或方法名。
3、考虑最极端的一种情况,就连一个@Qualifier都无法唯一确定一个bean,这时可能会想到用多个@Qualifier限定,然而Java8之前不允许同一注解在一处出现多次,Java8只允许带有@Repeatable的注解出现多次,而@Qualifier没有@Repeatable修饰。这是另外一个解决办法就是封装@Qualifier进行自定义注解,它可以根据我们具体业务无限叠加规则,示例如下:
//自定义限定注解 @Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface MySql { } //使用限定注解 @Configuration public class Config { @Bean(name = "prod-db") @Profile("prod") @Qualifier("prod")//bean的限定符,在多个同类型的bean存在时Qualifier通过指定限定符区分,默认与bean id一致 @Mysql//自定义注解限定bean public String prodStr(){ return "prod-mysql"; } }
4、理所当然对应的XML配置:最烦XML
<bean id="xxx" class="com.xxx.xxx" primary="true"> <!--Some properties configuration--> </bean> <bean id="xxx" class="com.xxx.xxx"> <qualifier type="com.xxx.xxx" value="qualifier-id"/> </bean>
四、Bean的作用域
Spring默认的bean都是单例模式,无论注入多少次,上下文中始终只存在一个实例对象。当某些bean与状态有关时,单例就不合理了,Java对象会被污染。这时Spring提供了一个作用域概念@Scope设置bean的作用域,具体取下:
- 单例(Singleton):在整个应用中,只创建一个bean的实例。
- 原型(Prototype):每次注入或通过Spring上下文获取bean的时候,都创建一个新实例。
- 会话(Session):在Web应用中为每个会话创建一个实例。
- 请求(request):在Web应用中为每个请求创建一个实例。
前两种很好理解,后两种用于web应用中,相应的还要配置代理模式(proxyMode),后两种不详细说明,后面再记录。在JavaConfig中使用@Scope注解实现:
@Configuration public class Config { @Bean(name = "dev-db") @Profile("dev") @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)//单例 public String devStr(){ return "dev"; } }
同样XML实现:
<bean id="xxx" class="com.xxx.xxx" scope="prototype"> <!--Some properties configuration--> </bean> <bean id="xxx" class="com.xxx.xxx" scope="session"> <aop:scoped-proxy/><!--设置代理模式--> <!--Some properties configuration--> </bean>
五、运行时注入
很多时候,我们需要在配置文件中为某些对象的属性指定值,比如数据源、用户名和密码等。这时候就会用到Spring的运行时注入技术,Spring提供的运行时注入方式包括属性占位符(Property placeholder)和Spring表达式语言(SpEL)。
1、属性占位符:属性占位符即形如"${}",大括号中填写在配置文件(xxx.properties or anything else)中定义的变量名。通常使用时在@Value中添加占位符,如@Value("${db.url}"),然后在配置文件中定义好相关变量,最后在配置类中用@PropertySource("xxx")注解引入配置文件即可。示例代码如下:
@Configuration @PropertySource("classpath:app.properties") public class MagicConfig { @Autowired Environment evn;//通过environment进行运行时注入 @Value("${person.name}") String name; @Bean @Qualifier("jack") public String jack(@Value("${person.age}") Integer age) { return name + " is " + age + " years old, and his weight is " + evn.getProperty("person.weight", Float.class, 0.0F) + " kg."; } }
正如你所看到的那样,上面代码还使用了Spring的Environment来获取属性文件中定义的值,这是因为使用@PropertySource时将属性文件加载到了Spring Environment中,因此后来就可以使用Environment bean来获取我们定义的属性值。Spring Environment有着强大的作用,具体用法不在此处详解。使用占位符的XML配置如下:
<context:property-placeholder/><!--生成PropertySourcesPlaceHolderConfigurer bean--> <bean id="xxx" class="com.xxx.xxx" p:name="${person.name}"/>
2、Spring表达式语言(SpEL)进行装配,既然它可以作为一门“语言”,强大的功能毋庸置疑。与占位符类似,SpEL以形如"#{}"的形式出现,最简单的SpEL就是"#{1}",它就代表数字1,接下来看一下SpEL的常见用法。
- #{T(JavaClass).staticMethod}:T()表达式会将()中的内容视为Java中的类,因此可直接调用其static方法,如#{T(System).currentTimeMillis()}。
- #{3.14}:它表示字面常量,还有字符串类型的#{'abc'},科学记数法#{2.13E4},bool类型#{false}等。
- #{bean-id}:它可以直接通过bean-id去引用一个bean,注意没有引号,加引号就是字符串常量。
- #{bean.property}:它可以引用某个bean中的某个属性,如#{jack.age}可以获取java这个bean的name属性的值。
- #{bean.method()}:它可以调用某个bean上的方法,如#{jack.eat()},返回值可以通过@Value注入到其他变量中。此外还可以向Java中那样级联调用,如#{jack.getName().toUpperCase()}。
- "?."运算符:"?."运算符可以确保在访问它右边的内容之前,左边返回值不是null,如果是null则右边不会执行,转而整个表达式直接返回null。如#{jack.getName()?.toUpperCase()}可以判定getName返回的是不是null,不是null则继续执行toUpperCase,否则直接返回null。
- SpEL运算符:其中条件运算可用于三元运算,如#{a==b?c:d};另外还可以检查null值,如果为null就用一个默认值替换,如#{jack.gf ?: 'alice'}。
运算符类型 运算符 算术运算 +、-、*、/、%、^ 比较运算 <、>、==、<=、>=、lt、gt、eq、le、ge 逻辑运算 and、or、not、| 条件运算 ?:(ternary)、?:(Elvis) 正则表达式 matches (如:#{1234 matches '[0-9]+'})
- #{bean.list[2].property}:SpEL可以引用数组或列表中的一个元素,下标从0开始,如#{jukebox.songs[2].title}表示jukebox专辑的第3音乐的标题。另外字符串也是一个集合,如#{'abc'[2]}表示第3个字母c。
- .?[]:SpEL查询运算符,它会对集合进行过滤得到一个子集。如#{jukebox.songs.?[artist == 'Aerosmith']}表示只要jukebox专辑中艺术家为Aerosmith的音乐,[]中是筛选条件。
- .^[]和.$[]:它们可以查询列表中第一个匹配和最后一个匹配的条目,如#{jukebox.songs.^[artist == 'Aerosmith']}表示查找jukebox专辑中第一个艺术家为Aerosmith的音乐。
- .![]:它是SpEL中的投影运算,它会从集合的每个成员中选取特定的属性放到一个新的集合中,如#{jukebox.songs.![title]}表示将jukebox中所有音乐的标题提取出来。
SpEL的内容远不止这些,有兴趣可以深入学习,Spring任重而道远,继续努力,本次笔记到此结束!!!

浙公网安备 33010602011771号