读Spring实战(第四版)概括—高级装配

时间:2022-02-13 10:46:29

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。

读Spring实战(第四版)概括—高级装配

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配置:

先添加外部配置文件:

读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>