Spring in Action
基础知识
Spring采用的关键策略:
- 基于POJO的轻量级和最小侵入性编程
- 通过依赖注入和面向接口实现松耦合
- 基于切面和惯例进行声明式编程
- 通过切面和模板减少样板式代码
通过DI,对象的依赖关系将由系统中负责协调个对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系,依赖关系将被自动注入到需要他们的对象当中去。
创建应用组件之间协作的行为通常称为装配(wiring).
Spring容器并不是只有一个。Spring自带了多个容器实现,可以归纳为两类:Bean工厂(由org.springframework.beans.factory.BeanFactory接口定义)是最简单的容器,提供基本的DI支持。应用上下文(由org.springframework.context.ApplicationContext接口定义)基于BeanFactory构建,并提供应用框架级别的服务,例如从属性文件解析文本信息以及发布应用给感兴趣的事件监听者。
AnnotationConfigApplicationContext:从一个或者多个基于Java的配置类中加载Spring的应用上下文
AnnotationConfigWebApplicationContext:从一个或者多个基于Java的配置类中加载Spring Web的应用上下文
ClassPathXmlApplicationContext:从类路径下的一个或者多个xml配置文件中加载上下文定义,把应用上下文的定义文件作为类资源(在所有的类路径(包含JAR文件)下查找指定的xml文件)
FileSystemXmlApplicationContext:从文件系统下的一个或多个xml配置文件中加载上下文定义
XmlWebApplicationContext:从WEB应用下的一个或者多个xml配置文件中加载上下文定义。
Dependency Injection
装配机制:
- 在XML中进行显示配置
- 在Java中进行显示配置
- 隐式的bean发现机制和自动装配
自动化装配Bean
- 组件扫描:Spring会自动发现应用上下文中所创建的bean
- 自动装配:Spring自动满足bean之间的依赖
@ComponentScan:如果没有其他的配置,此注解会扫描与配置类相同的包及其所有子包。
如果使用XML配置的方式来启动组件扫描,则可使用
<context:component-scan base-package="xxx.xxx.xx">
@Component注解后,Spring会根据类名为其指定一个ID,即将类名的第一个字母变为小写。Spring支持将@Named注解作为@Component注解的替代方案。
@Component注解的三个重要属性:
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
@Autowired和@Inject绝大多数场景下可以相互替换。
通过Java代码装配Bean
@Bean注解属性:
String[] name() default {}; 默认情况下,bean的ID与带有@Bean注解的方法名是一样的。
Autowire autowire() default Autowire.NO;
String initMethod() default "";
String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
默认情况下,Spring的bean都是单例的。
通过XML装配Bean
<bean id="" class=""/>
借助构造器注入初始化bean
<constructor-arg>
使用Spring3.0引入的c-命名空间
对于强依赖使用构造器注入,对于可选性依赖,使用属性注入。
属性注入
<property ...>
使用Spring3.0引入的p-命名空间
举例util:
<bean id="compactDisc" class="soundsystem.properties.BlankDisc"
p:title="Sgt. Pepper's Lonely Hearts Club Band"
p:artist="The Beatles"
p:tracks-ref="trackList"/>
<util:list id="trackList">
<value>Sgt. Pepper's Lonely Hearts Club Band</value>
<value>With a Little Help from My Friends</value>
<value>Lucy in the Sky with Diamonds</value>
<value>Getting Better</value>
<value>Fixing a Hole</value>
<value>She's Leaving Home</value>
<value>Being for the Benefit of Mr. Kite!</value>
<value>Within You Without You</value>
<value>When I'm Sixty-Four</value>
<value>Lovely Rita</value>
<value>Good Morning Good Morning</value>
<value>Sgt. Pepper's Lonely Hearts Club Band (Reprise)</value>
<value>A Day in the Life</value>
</util:list>
<bean id="cdPlayer" class="soundsystem.properties.CDPlayer" p:compactDisc-ref="compactDisc"/>
util-命名空间中的元素:
<util:constant>
引用某个类型的public static域,并将其暴露为bean.
<util:property-path>
引用一个bean的属性(或内嵌属性),并将其暴露为bean.
混合配置
@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath:cd-config.xml")
public class SoundSystemConfig {
}
高级装配
在Spring3.1中,只能在类级别上使用@Profile注解,不过从Spring3.2开始,在方法级别上也可以使用@Profile注解,与@Bean注解一同使用。
@Configuration
public class DataSourceConfig {
@Bean(destroyMethod = "shutdown")
@Profile("dev")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
@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();
}
}
注意:没有指定profile的bean始终都会被创建,与激活哪个profile没有关系。
以上是Java注解式配置的方式,下面是XML配置的方式:
<?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"
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
设置这两个属性值得方式:
- 作为DispatcherServlet的初始化参数;
- 作为Web应用的上下文参数;
- 作为JNDI条目;
- 作为环境变量;
- 作为JVM的系统属性;
- 在集成测试类上,使用@ActiveProfiles注解设置。
代码样例:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 为上下文设置默认的profile -->
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</context-param>
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>com.lvmama.servlet.DispatcherServletLv</servlet-class>
<!-- 为Servlet设置默认的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>
注意点:可以同时激活多个profile,通过列出多个profile名称,并以逗号分隔来实现。
使用profile进行测试@ActiveProfiles:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=DataSourceConfig.class)
@ActiveProfiles("dev")
public class DevDataSourceTest {
}
条件化的Bean(Spring4.0之后才出现的)
@Conditional
相关接口:Condition
public interface Condition {
/**
* Determine if the condition matches.
*/
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
public interface ConditionContext {
/**
* BeanDefinitionRegistry检查bean定义
*/
BeanDefinitionRegistry getRegistry();
/**
* ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性
*/
ConfigurableListableBeanFactory getBeanFactory();
/**
* Environment检查环境变量是否存在以及他的值是什么
*/
Environment getEnvironment();
/**
* 读取并探查ResourceLoader所加载的资源
*/
ResourceLoader getResourceLoader();
/**
* ClassLoader加载并检查类是否存在
*/
ClassLoader getClassLoader();
}
AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有什么其他的注解。
public interface AnnotatedTypeMetadata {
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);
}
@Profile注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
/**
* The set of profiles for which the annotated component should be registered.
*/
String[] value();
}
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
if (context.getEnvironment() != null) {
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;
}
}
处理自动装配的歧义性
第一种方式: @Primary注解能够与@Component可以一起用在组件扫描的bean上,也可以与@Bean注解组合用在Java配置的Bean声明中。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Primary {
}
XML配置的方式:
<bean id="iceCream" class="com.vincent.IceCream" primary="true"/>
第二种方式: @Qualifier注解是使用限定符的主要方式;他可以与@Autowired和@Inject协同使用。在注入的时候指定想要注入的进去的是哪个bean.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {
String value() default "";
}
第三种方式:使用自定义的注解,只不过自定义注解上会使用@Qualifier注解。
Bean的作用域
- 单例(singleton):在整个应用中,只创建bean的一个实例;
- 原型(prototype):每次注入或者通过spring的上下文获取的时候,都会创建一个新的bean实例。
- 会话(session):在web应用中,为每个会话创建一个bean实例;
- 请求(request):在web应用中,为每个请求创建一个bean实例。
使用@Scope注解(它可以与@Component与@Bean一起使用):
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
/**
* Specifies the scope to use for the annotated component/bean.
* @see ConfigurableBeanFactory#SCOPE_SINGLETON
* @see ConfigurableBeanFactory#SCOPE_PROTOTYPE
* @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
* @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
*/
String value() default ConfigurableBeanFactory.SCOPE_SINGLETON;
/**
* Specifies whether a component should be configured as a scoped proxy
* and if so, whether the proxy should be interface-based or subclass-based.
* <p>Defaults to {@link ScopedProxyMode#NO}, indicating that no scoped
* proxy should be created.
* <p>Analogous to {@code <aop:scoped-proxy/>} support in Spring XML.
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}
XML的配置方式:
<bean class="com.myapp.Notepad" scope="prototype" />
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean class="com.myapp.Notepad" scope="prototype" />
<bean class="com.myapp.UniqueThing" scope="session">
<aop:scoped-proxy proxy-target-class="true"/>
</bean>
</beans>
Spring提供了两种在运行时求值的方式:
- 属性占位符
- Spring表达式语言(SpEL)
第一种使用@PropertySource注解加载属性文件。
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
public class EnvironmentConfig {
@Autowired
Environment env;
@Bean
public BlankDisc blankDisc() {
return new BlankDisc(env.getProperty("disc.title"), env.getProperty("disc.artist"));
}
}
第二种:Spring一直支持将属性定义到外部的属性的文件中,并使用属性占位符值将其插入到Springbean中,在Spring装配中,占位符的形式为使用“${ … }”包装的属性名称。
<?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:c="http://www.springframework.org/schema/c"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<context:property-placeholder location="com/soundsystem/app.properties"/>
<bean class="com.soundsystem.BlankDisc"
c:_0="${disc.title}"
c:_1="${disc.artist}"/>
</beans>
如果依赖于组件扫描和自动装配来创建和初始化应用组件,那么就没有指定占位符的配置文件或类了,在这种情况下,可以使用@Value注解。
public BlankDisc(@Value("${disc.title}") String title, @Value("${disc.artist}") String artist) {
this.title = title;
this.artist = artist;
}
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
SpEL
特性:SpEL表达式要放到”#{ … }“中
- 使用bean的ID来引用bean
- 调用方法和访问对象的属性
- 对值进行算数、关系和逻辑运算
- 正则表达式匹配
- 集合操作
#{artistSelector.selectArtist()?.toUpperCase()}
如果要在SpEL中访问类作用域的方法和常量的时候,需要使用T()运算符
T(java.lang.Math).PI
#{admin.email matches '[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\\.com'}
4