1.环境与Profile
在开发软件的时候,有时候需要从一个环境迁移到另一个环境。比如在开发阶段我们使用的是dev的环境,在测试阶段使用的是product环境,这时我们就需要不同的配置。Spring同样也提供了类似的解决方案(在Spring3.1中引入了bean profile功能)。如下所示,是一个使用@Profile注解来实现的实例。
首先需要准备两个配置类:
// ProdConfiguration.java @Configuration @Profile("prod") public class ProdConfiguration { @Bean public ProdE getProdE(){ ProdE prodE = new ProdE(); prodE.setContent("产品"); return prodE; } } // DevConfiguration.java @Configuration @Profile("dev") public class DevConfiguration { @Bean public DevE getDevE() { DevE devE = new DevE(); devE.setContent("开发"); return devE; } }
再需要在初始化Servlet环境配置类(用于替换web.xml)中设置profile值(下划线)
public class HelloWorldInitializer implements WebApplicationInitializer { public void onStartup(ServletContext container) throws ServletException { AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); ctx.register(HelloWorldConfiguration.class); ctx.setServletContext(container); ctx.getEnvironment().setActiveProfiles("dev"); ServletRegistration.Dynamic servlet = container.addServlet( "dispatcher", new DispatcherServlet(ctx)); servlet.setLoadOnStartup(1); servlet.addMapping("/"); } }
最后用一个Controller来测试一下:
@Controller @RequestMapping("/") public class HelloWorldController { @Autowired(required=false) ProdE prodE; @Autowired(required=false) DevE devE; @RequestMapping(method = RequestMethod.GET) public String test(ModelMap model) { if(null!=prodE){ System.out.println("******"+prodE.getContent()+"******"); }else{ System.out.println("******prodE is null******"); } if(null!=devE){ System.out.println("******"+devE.getContent()+"******"); }else{ System.out.println("******devE is null******"); } model.addAttribute("greeting", "Hello World from Spring 4 MVC"); return "welcome"; } }
当然也可以使用XML配置,如下所示:
<beans profile="prod"> <!-- 在这里配置bean --> </beans> <beans profile="dev"> <!-- 在这里配置bean --> </beans>
从Spring3.2开始已经支持在方法级别上使用@Profile注解,与@Bean注解一起使用,这样就可以将这两种声明放置在同一个配置类中。
2.激活使用profile
如果不激活使用profile,你配置的profile也是不会被使用的,因为Spring不能确定哪个profile是需要使用的。Spring提供了两个独立的属性来构成一个比较合理的设置:spring.profiles.active和spring.profiles.default。如果设置了spring.profiles.active属性,则Spring会设置本值,如果spring.profiles.active没有设置,则使用spring.profiles.default的默认值。spring.profiles.active对应于注解中的ConfigurableEnvironment.setActiveProfiles(String... profiles);spring.profiles.default对应于注解中的ConfigurableEnvironment.setDefaultProfiles(String... profiles);我们可以有多种方式设置这两种属性:
- 作为DispatcherServlet的初始化参数
- 作为Web应用上下文条目参数
- 作为JNDI条目
- 作为环境变量
- 作为JVM的系统变量
- 在集成测试类上,使用@ActiveProfiles注解设置
XML中的配置如下所示:
<?xml version="1.0" encoding="ISO-8859-1"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <display-name>test</display-name> <!-- - Location of the XML file that defines the root application context. - Applied by ContextLoaderListener. --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/application-config.xml</param-value> </context-param> <!-- 为上下文设置默认的profile --> <context-param> <param-name>spring.profiles.default</param-name> <param-value>dev</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/mvc-config.xml</param-value> </init-param> <!-- 为DispatcherServlet设置默认的profile --> <init-param> <param-name>spring.profiles.default</param-name> <param-value>dev</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
3.在Springboot中使用Profile
在Springboot中它允许你通过命名约定按照一定格式来定义多个配置文件,文件格式如下:application-{profile}.properties,比如:application-dev.properties(开发环境)、application-test.properties(测试环境)、application-prod.properties(生产环境)。我们只需要在application.properties中指定spring.profiles.defualt属性的值或者在jar包运行时增加--spring.profiles.active=XXX属性即可(也可以在外部配置文件覆盖内部application.properties文件中spring.profiles.defualt属性),@Profile注解使用方式和以前相同。
4.条件注解@Conditional
在有些场景下,我们希望当要创建的Bean满足一系列条件后才创建。Spring4中新增了@Conditional注解,它可以用到带有@Bean注解的方法上,为我们提供这一功能。当给定的条件满足时,返回true,创建Bean;当给定的条件不满足时,返回false,不创建Bean。样例如下所示(以获取本机操作系统来区分是否要加载配置为例):
首先需要写判定条件,实现Condition接口:
public class LinuxCondition implements Condition { public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // 判断当前操作系统是否是Linux return context.getEnvironment().getProperty("os.name").contains("Linux"); } } public class WindowsCondition implements Condition { public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // 判断当前操作系统是否是windows return context.getEnvironment().getProperty("os.name").contains("Windows"); } }
再只需要在@Bean注解(配置类)过的方法上使用该注解即可:
@Bean @Conditional(WindowsCondition.class) public WindowsE getWin() { WindowsE e = new WindowsE(); System.out.println("WindowsE Bean Is Created."); return e; } @Bean @Conditional(LinuxCondition.class) public LinuxE getLinux() { LinuxE e = new LinuxE(); System.out.println("LinuxE Bean Is Created."); return e; }
设置给@Conditional的类可以是任意实现了Condition接口的类型。这个接口,如下所示,只需实现matches()方法实现即可。matches()方法会使用ConditionContext和AnnotatedTypeMetadata对象来做决策。
public interface Condition { boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }
4.1.ConditionContext条件判断上下文
通过ConditionContext类,我们可以做到以下几点:
-
借助getRegistry()方法返回BeanDefinitionRegistry检查Bean定义
-
借助getBeanFactory()方法返回ConfigurableListableBeanFactory检查Bean是否存在,甚至探查Bean的属性
借助getEnvironment()方法返回Environment检查环境变量是否存在和值
借助getResourceLoader()方法返回ResourceLoader所加载的资源
借助getClassLoader()方法返回ClassLoader加载并检查类是否存在
4.2.AnnotatedTypeMetadata获取其他属性注解
AnnotatedTypeMetadata的isannotated()方法可以判断被@Bean注解的方法是否有其他注解属性,通过getAllAnnotationAttibutes(...)方法获取所有的注解属性。除此之外,Spring 4中也对之前的@Profile注解进行的重写,如下所示:(@Conditional注解调用了ProfileCondition条件)
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(ProfileCondition.class) public @interface Profile { String[] value(); }
@Profile注解其实也是使用了@Conditional注解,并且引用了ProfileCondition作为Condition的实现,如下所示。我们可以看到,ProfileCondition通过AnnotatedTypeMetadata获取使用了@Profile注解的所以属性。借助该属性,它会明确地检查value属性,该属性包含bean的profile的value值,比如@Profile(“dev”)。然后,它通过CondidationContext得到的Environment来检查该profile是否处于被激活状态。
5.处理自动装配的歧义性
虽然在实际编写代码中,很少有情况会遇到Bean装配的歧义性,更多的情况是给定的类只有一个实现,这样自动装配就会很好的实现。但是当发生歧义性的时候,Spring提供了多种的可选解决方案。如下所示,我们有chinesefood和asanfood继承自父类Food,装配选择哪种Food时会出现NoUniqueBeanDefinitionException:
如果我们首选阿三food,就如下所示,在类或者配置中bean配置的方法上添加@Primary注解即可:
@Component @Primary public class ASanFood implements Food { public void context() { System.out.println("ASanFood"); } } // ——————或者—————— @Bean @Primary public Food getFood(){ return new ASanFood(); }
注意,对于多可选择项,只能有一个可以加上@Primary
当然可以使用另一种方式解决歧义性@Qualifier比如在类上需要装配Food类。
@Autowired @Qualifier("chineseFood") Food food;
@Qualifier("chineseFood")指向的是扫描组件时创建的Bean,并且这个Bean是IceCream类的实例。事实上如果所有的Bean都没有自己指定一个限定符(Qualifier),则会有一个默认的限定符(与Bean ID相同),我们可以在Bean的类上添加@Qualifier注解来自定义限定符,如下所示:
@Component @Qualifier("asFood") public class ASanFood implements Food { public void context() { System.out.println("ASanFood"); } } // 使用时: @Autowired @Qualifier("asFood") Food food;
6.Bean作用域
默认情况下,Spring应用上下文中所有的Bean都是单例模式。在大多数情况下单例模式都是非常理想的方案。但是如果,你要注入或者装配的Bean是易变的,他们会有一些特有的状态。这种情况下单例模式就会容易被污染。Spring为此定义了很多作用域,可以基于这些作用域创建Bean,包括:
-
单例(Singleton):在整个应用中,只创建Bean的一个实例
-
原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的Bean实例。这个相当于new的操作
会话(Session):在Web应用中,为每个会话创建一个Bean实例。对于同一个接口的请求,如果使用不同的浏览器,将会得到不同的实例(Session不同)
请求(Request):在Web应用中,为每个请求创建一个Bean实例
使用@Scope注解可以设置多种的作用域,可以放置在类上或者@Bean注解的方法上,如下所示,是一个基本的实例:
@Component @Scope("prototype") public class Car { // 。。。。。。 } // ——————或者—————— @Bean @Scope("prototype") public LinuxConfig getLinux() { LinuxConfig config = new LinuxConfig(); return config; }
6.1.使用会话和请求作用域
在实际企业级开发过程中,我们常用@Scope来定义Bean的作用域。如用户的购物车信息,如果将购物车类声明为单例(Singleton),那么每个用户都向同一个购物车中添加商品,这样势必会造成混乱;你也许会想到使用原型模式声明购物车,但这样同一用户在不同请求时,所获得的购物车信息是不同的,这也是一种混乱。如下所示:
@Bean @Scope( value="session", proxyMode=ScopedProxyMode.INTERFACES ) public Cart getCart() { // ...... }
在这里我们要注意一下,属性proxyMode。这个属性解决了将会话或者请求作用域的Bean注入到单例Bean中所遇到的问题。假设我们要将Cart bean注入到单例StoreService bean的Setter方法中:
@Service @Scope public class StoreService { Cart cart; @Autowired public void setCart(Cart cart) { this.cart = cart; } // ...... }
StoreService是一个单例bean,会在Spring应用上下文加载的时候创建。 当它创建的时候, Spring会试图将Cart bean注入到setCart()方法中。 但是Cart bean是会话作用域的, 此时并不存在。 直到某个用户进入系统,创建了会话之后,才会出现Cart实例。系统中将会有多个Cart实例: 每个用户一个。 我们并不想让Spring注入某个固定的Cart实例到StoreService中。 我们希望的是当StoreService处理购物车功能时, 它所使用的Cart实例恰好是当前会话所对应的那一个。Spring并不会将实际的Cart bean注入到StoreService中,Spring会注入一个到Cart bean的代理。这个代理会暴露与Cart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用Cart的方法时, 代理会对其进行懒解析并将调用委托给会话作用域内真正的Cart bean。
proxyMode属性被设置成了ScopedProxyMode.INTERFACES, 这表明这个代理要实现Cart接口,并将调用委托给实现bean。如果Cart是接口而不是类的话,这是可以的(也是最为理想的代理模式)。但如果Cart是一个具体的类的话,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。
下面是使用XML来配置Bean的作用域:
<bean class="com.wxh.entity.Cart" scope="session"> <aop:scoped-proxy/> </bean>
XML中的<aop:scoped-proxy/>代表的是注解中的proxyMode属性,默认情况下是使用CGLIB代理,如果要使用接口代理,需要手动关闭,如下所示:
<bean class="com.wxh.entity.Cart" scope="session"> <aop:scoped-proxy proxy-target-class="false"/> </bean>
7.运行时值注入
Bean的属性注入的时候有时候硬编码是可以的,但是有时候我们希望避免硬编码值,而是想让这些在运行时再确定。为了实现这些功能,Spring提供了两种在运行时的求值方式:
-
属性占位符(Property Placeholder)
-
Spring表示式语言(SpEL)
这两种技术的用法是类似的,不过他们的目的和行为是有所差别的。前者比较简单,后者比较难。
7.1.注入外部值
在Spring中,处理外部值得最简单方式就是声明属性源并通过Spring的Environment来检索属性。例如下面所示,展示了最基本的Spring配置:
先添加外部配置文件:
在配置类中引用这个外部配置文件,并使用:
@Configuration // 声明属性源 @PropertySource("classpath:app.properties") public class ExConfig { @Autowired Environment env; @Bean public Cart getCart() { // 检索配置的属性值 env.getProperty("cart.context"); // ...... } }
使用@PropertySource注解会将app.properties配置文件加载到Spring的Environment中,这里我们将详细讲一下Environment这个接口(Environment实现自PropertyResolver接口)。主要的方法如下:
- String getProperty(String key):通过key获取值
- String getProperty(String key, String defaultValue):通过key获取值,如果没有值,则使用默认值替换
- <T> T getProperty(String key, Class<T> targetType):通过key获取值,但是可以返回非String类型的值,比如Integer等
- <T> T getProperty(String key, Class<T> targetType, T defaultValue):通过key获取值,但是可以返回非String类型的值,比如Integer等。如果值不存在则使用默认值
- String getRequiredProperty(String key) throws IllegalStateException:获取必要的属性,如果不存在则报错
- <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException:同上,不过可以返回除String外的其他类型的值
- boolean containsProperty(String key):是否存在
- String[] getActiveProfiles():返回激活的profile名称的数组
- String[] getDefaultProfiles():返回默认profile名称和数组
- boolean acceptsProfiles(String... profiles):如果Environment支持给定profile的话,返回true
7.2.解析属性占位符
直接从Environment中检索属性是非常方便的,尤其是在Java配置中装配bean的时候。但是Spring也提供了通过占位符装配属性的方法,这些占位符的值会来源于一个属性源。Spring一直支持将属性值定义到外部的属性文件中,并使用占位符将其插入到Spring Bean中。在Spring装配中,占位符的形式为使用“${...}”包装的属性名。使用方式如下所示:
先需要配置一个PropertySourcesPlaceholderConfigurer类,因为它能基于Spring Envrionment及其属性源来解析占位符。
@Bean public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); }
然后在配置类中使用@Value注解:
@Bean public Cart getCart(@Value("${cart.context}") String context) { // ...... }
当然也可以使用XML来配置,如下所示:
先加属性占位符解析类:
<context:property-placeholder/>
在使用XML配置或者@Value注解都可以
<bean class="com.wxh.entity.Cart"> <property name="context" value="${cart.context}"></property> </bean>