《Spring 实战》读书笔记 - 第 3 章 高级装配,全网最具深度的三次握手、四次挥手讲解
在开发软件的时候,有一个很大的挑战就是将应用程序从一个环境迁移到另外一个环境。开发阶段中,某些环境相关做法可能并不适合迁移到生产环境中,甚至即便迁移过去也无法正常工作。数据库配置、加密算法以及与外部系统的集成是跨环境部署时会发生变化的几个典型例子。
比如,考虑一下数据库配置。在开发环境中,我们可能会使用嵌入式数据库,并预先加载测试数据。
数据源的有三种连接配置,分别是
// 通过 EmbeddedDatabaseBuilder 会搭建一个嵌入式的 Hypersonic 的数据库
@Bean(destroyMethod = "shutdown")
@Profile("dev")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
// 通过 JNDI 获取 DataSource 能够让容器决定该如何创建这个 DataSource
@Bean
@Profile("prod")
public DataSource jndiDataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
// 还可以配置为 Commons DBCP 连接池,BasicDataSource 可替换为阿里的 DruidDataSource 连接池
@Bean(destroyMethod = "close")
@Profile("qa")
public DataSource datasource(){
BasicDataSource datasource = new BasicDataSource();
datasource.setUrl("jdbc:h2:tcp://dbserver/~/test");
datasource.setDriverClassName("org.h2.Driver");
datasource.setUsername("sa");
datasource.setPassword("password");
datasource.setInitialSize(20);
datasource.setMaxActive(30);
return dataSource;
}
Spring 为环境相关的 bean 所提供的解决方案不是在构建的时候做出决定,而是等待运行时再来确定。Spring 引入了 bean 的 profile 的功能,在每个数据库连接配置的 bean 上添加 @Profile,指定这个 bean 属于哪一个 profile。
Spring3.1 需要将 @Profile 指定在配置类上,Spring3.2 就可以指定在方法上了。
我们也可以在 XML 中通过<bean>
元素的 profile 属性指定。例如:
<?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" type="H2">
<jdbc:script location="classpath:schema.sql" />
<jdbc:script location="classpath:test-data.sql" />
</jdbc:embedded-database>
</beans>
<beans profile="prod">
<jee:jndi-lookup id="dataSource"
lazy-init="true"
jndi-name="jdbc/myDatabase"
resource-ref="true"
proxy-interface="javax.sql.DataSource" />
</beans>
</beans>
下一步就是激活某个 profile
Spring 在确定哪个 profile 处于激活状态时,需要依赖两个独立的属性:spring.profiles.active 和 spring.profiles.default。如果设置了 spring.profiles.active 属性的话,那么它的值就会用来确定哪个 profile 是激活的,但如果没有设置 spring.profiles.active 属性的话,那 Spring 将会查找 spring.profiles.default 的值。如果 spring.profiles.active 和 spring.profiles.default 均没有设置的话,那就没有激活的 profile,因此只会创建那些没有定义在 profile 中的 bean。
有多种方式来设置这两个属性:
作为 DispatcherServlet 的初始化参数;
作为 Web 应用的上下文参数;
作为 JNDI 条目;
作为环境变量;
作为 JVM 的系统属性;
在集成测试类上,使用 @ActiveProfiles 注解设置。
例如,在 web 应用中,设置 spring.profiles.default 的 web.xml 文件会如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<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>appServlet</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<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>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Spring4 实现了条件化配置,需要引入 @Conditional(可以用到带有 @bean 注解的方法上)注解。如果给定条件为 true,则创建这个 bean,反之,不创建。
例如:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MagicConfig {
@Bean
@Conditional(MagicExistsCondition.class) // 条件化创建 bean
public MagicBean magicBean() {
return new MagicBean();
}
}
@Conditional 中给定了一个 Class,它指明了条件——本例中是 MagicExistsCondition。@Conditional 将会通过 Condition 接口进行条件对比:
public interface Condition {
boolean matches(ConditionContext ctxt AnnotatedTypeMetadata metadata);
}
接下来是 MagicExistsCondition 的实现类:
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class MagicExistsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
// 根据环境中是否存在 magic 属性来决策是否创建 MagicBean
return env.containsProperty("magic");
}
}
ConditionContext 是一个接口,大致如下所示:
public interface ConditionContext {
BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader();
}
ConditionContext 实现的考量因素可能会更多,通过 ConditionContext,我们可以做到如下几点:
借助 getRegistry() 返回的 BeanDefinitionRegistry 检查 bean 定义;
借助 getBeanFactory() 返回的 ConfigurableListableBeanFactory 检查 bean 是否存在,甚至探查 bean 的属性;
借助 getEnvironment() 返回的 Environment 检查环境变量是否存在以及它的值是什么;
读取并探查 getResourceLoader() 返回的 ResourceLoader 所加载的资源。
借助 getClassLoader() 返回的 ClassLoader 加载并检查类是否存在。
AnnotatedTypeMetadata 则能够让我们检查带有 @Bean 注解的方法上还有什么其他的注解,它也是一个接口,如下所示:
public interface AnnotatedTypeMeta {
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);
}
借助 isAnnotated()方法,能够判断带有 @Bean 注解的方法是不是还有其他特定的注解。
当自动装配 bean 时,遇到多个实现类的情况下,就出现了歧义,例如:
@Autowired
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
Dessert 是一个接口,并且有三个类实现了这个接口,如下所示:
@Component
public class Cake implements Dessert { ... }
@Component
public class Cookies implements Dessert { ... }
@Component
public class IceCream implements Dessert { ... }
三个实现均使用了 @Component,在组件扫描时,能够创建它们的 bean。但 Spring 试图自动装配 setDessert()中的 Dessert 参数是,它并没有唯一、无歧义的可选值,Spring 无法做出选择,则会抛出 NoUniqueBeanDefinitionException 的异常。
两种解决办法:
第一种方法:标示首选的 bean
如下所示:
@Component
@Primary
public class IceCream implements Dessert { ... }
或者,如果通过 JavaConfig 配置,如下:
@Bean
@Primary
public Dessert iceCream() {
return new IceCream();
}
或者,使用 XML 配置 bean 的话,如下:
<bean id="iceCream" class="com.desserteater.IceCream" primary="true" />
需要注意的是:不能标示两个或更多的首选 bean,这样会引来新的歧义。
第二种方法:限定自动装配的 bean
如下所示:
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
如果不想用默认的 bean 的名称,也可以创建自定义的限定符
@Component
@Qualifier("cold")
public class IceCream implements Dessert { ... }
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
或者使用 JavaConfig 配置
@Bean
@Qualifier("cold")
public Dessert iceCream() {
return new IceCream();
}
如果出现多个 Qualifier,尝试为 bean 也标示多个不同的 Qualifier 来表明要注入的 bean。
@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert { ... }
@Component
@Qualifier("cold")
@Qualifier("fruity")
public class Popsicle implements Dessert { ... }
@Autowired
@Qualifier("cold")
@Qualifier("creamy")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
但有个问题,Java 不允许在同一个条目上重复出现相同类型的注解,编译器会提示错误。
解决办法是我们可以自定义注解:
@Targe({ElementType.CONSTRUCTOR, ElementType.FIELD,
ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold { }
@Targe({ElementType.CONSTRUCTOR, ElementType.FIELD,
ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy { }
@Targe({ElementType.CONSTRUCTOR, ElementType.FIELD,
ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Fruity { }
重新标注 IceCream
@Component
@Cold
@Creamy
public class IceCream implements Dessert { ... }
@Component
@Cold
@Fruity
public class Popsicle implements Dessert { ... }
注入 setDessert() 方法
@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
默认情况下,Spring 应用上下文所有 bean 都是作为以单例的形式创建的。
Spring 定义了多种作用域,可以基于这些作用域创建 bean,包括:
单例(Singleton):在整个应用中,只创建 bean 的一个实例。
原型(Prototype):每次注入或者通过 Spring 应用上下文获取的时候,都会创建一个新的 bean 实例。
会话(Session):在 Web 应用中,为每个会话创建一个 bean 实例。
请求(Request):在 Web 应用中,为每个请求创建一个 bean 实例。
例如,如果你使用组件扫描,可以在 bean 的类上使用 @Scope 注解,将其声明为原型 bean:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad { ... }
或者在 JavaConfig 上声明:
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad {
return new Notepad();
}
或者在 XML 上声明:
<bean id="notepad" class="com.myapp.Notepad" scope="prototype" />
在 web 应用中,如果能够实例化在会话和请求范围内共享 bean,那将很有价值。例如:电子商务的购物车,会话作用域最为适合。
@Component
@Scope(value=WebApplicationContext.SCOPE_SESSION,
proxyMode=ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart { ... }
注入一个服务类
@Component
public class StoreService {
@Autowired
public void setShoppingCart (ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
}
因为 StoreService 是一个单例的 bean,会在 Spring 应用上下文加载的时候创建。当它创建的时候,Spring 会试图将 ShoppingCart bean 注入到 setShoppingCart() 方法中。但是 ShoppingCart bean 是会话作用域的,此时不存在。直到某个用户进入系统,创建了会话之后,才会出现 ShoppingCart 实例。
另外,系统中将会有多个 ShoppingCart 实例:每个用户一个。我们并不想让 Spring 注入某个固定的 ShoppingCart 实例到 StoreService 中。我们希望的是当 StoreService 处理购物车功能时,它所用的 ShoppingCart 实例恰好是当前会话所对应的那一个。
Spring 并不会将实际的 ShoppingCart bean 注入到 StoreService 中,Spring 会注入一个到 ShoppingCart bean 的代理,如下图。这个代理会暴露与 ShoppingCart 相同的方法,所以 StoreService 会认为它就是一个购物车。但是,当 StoreService 调用 ShoppingCart 的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的 ShoppingCart bean。
如果 ShoppingCart 是接口而不是类的话,就用 ScopedProxyMode.TARGET_INTERFACES(用 JDK 的代理)。如果是类而不是接口,就必须使用 CGLib 来生成基于类的代理,所以要用 ScopedProxyMode.TARGET_CLASS。
请求的作用域原理与会话作用域原理一样。
作用域代理能够延迟注入请求和会话作用域的 bean
也可用 XML 配置
<bean id="cart" class="com.myapp.ShoppingCart" scope="session" >
<aop:scoped-proxy />
</bean>
<aop:scoped-proxy />
是与 @Scope 注解的 proxy 属性功能相同的 Spri
ngXML 配置元素。它会告诉 Spring 为 bean 创建一个作用域代理。默认情况下,它会使用 CGLib 创建目标类的代理。我们也可以将 proxy-targe-class 属性设置为 false,进而要求生成基于接口的代理:
<bean id="cart"
class="com.myapp.ShoppingCart"
scope="session" >
<aop:scoped-proxy proxy-targe-class="false"/>
</bean>
我们之前在 javaConfig 配置中,配置了 BlankDisc:
@Bean
public CompactDisc sgtPeppers() {
return new BlankDisc (
"Sgt. Pepper's Lonely Hearts Club Band",
"The Beatles"
);
}
这种硬编码实现了要求,但有时我们希望避免,而是想让这些值在运行时再确定。为了实现这些功能,Spring 提供了两种在运行时求值的方式:
属性占位符 (Property placeholder)。
Spring 表达式语言(SpEL)。
在 Spring 中,最简单的方式就是声明属性源并通过 Spring 的 Environment 来检索属性。
评论