Spring 简介
# Spring 简介
Spring 基于 IOC(Inversion Of Control) 和 DI(Dependency Inject)。Spring 出现以前,类对象是用户手动创建的(例如使用 new)与组装的,而使用 Spring 框架后类对象将由 Spring 框架生成并由 Spring 控制对象的生命周期
Spring 组成
Spring 上下文
在基于 Spring 的应用中,对象生存于 Spring 容器(container)中,Spring 容器负责创建、装配、配置并管理对象的整个生命周期,从创建到死亡
Spring 容器归为两大类型:Bean 工厂和应用上下文,常用的是 Spring 上下文。Spring 自带了多种类型的应用上下文:
- AnnotationConfigApplicationContext
- 从 Java 配置类中加载 Spring 应用上下文
AnnotationConfigApplicationContext(com.springinaction.knights.config.KnightConfig.class);
- AnnotationConfigWebApplicationContext
- 与上同,用于加载 web 配置
- ClassPathXmlApplicationContext
- 默认从所有已知路径下查找并载入 xml 配置文件
- 给出文件名即可:
ClassPathXmlApplicationContext("knight.xml");
- FileSystemXmlapplicationcontext
- 从文件系统中的 xml 配置文件中加载上下文
- 需明确指明文件位置:
FileSystemXmlApplicationContext("c:/knight.xml");
- XmlWebApplicationContext
Bean 作用域
默认情况下,Spring 应用上下文中的所有 bean 都以单例(singleton) 模式创建。有时候,你所使用的类是易变的(mutable),它们会保持一些状态,因此重用是不安全的。Spring 提供了几种作用域,可以按照需求来创建 bean
- 单例(Singleton),默认作用域,只创建一个 bean 实例
- 原型(Prototype),每次注入或者通过 Spring 上下文获取时会创建一个新的 bean 实例
- 会话(Session),web 应用中,为每一个会话创建一个 bean
- 请求(Rquest),web 应用中,为每一个请求创建一个 bean
@Scope
使用 @Scope 注解可以设置 bean 的作用域:
@Component // @Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
// @Scope("prototype"),使用上面那行的方式更安全一些
public class Notepad { ... }
xml 配置形式如下:
<bean id="notepad" class="com.myapp.Notepad" scope="prototype" />
Spring 依赖注入
Spring 可以使用配置文件或者注解来实现依赖注入,下面以配置文件形式和注解形式来简单解释 IOC 与 DI
配置文件形式(XML)
需要 Spring 进行管理的对象:
public class Category {
private int id;
private String name;
// getter & setter func...
}
public class Product {
private int id;
private String name;
private Category category;
// getter & setter func...
}
Spring 配置文件(IOC & DI):
<?xml version=...
<!-- 同时指定 name 和 id,此时 id 为标识符,而 name 为 Bean 的别名,两者都可以找到目标Bean -->
<bean id="category" name="c" class="xyz.yearn.Category">
<property name="name" value="category 1" />
</bean>
<bean name="p" class="xyz.yearn.Product" primary="true">
<property name="name" value="product1" />
<property name="category" ref="c" />
</bean>
</beans>
位于 <bean>
标签内的配置用于声明 bean
从 Spring 上下文获取对象
public class TestSpring {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[] { "applicationContext.xml" });
// 对象 p 中的 category 由 name 为 c 的 bean 自动进行填充:ref="c",这是 DI
Product p = (Product) context.getBean("p"); // IOC,xml 配置文件中创建了一个 name 为 p 的对象
System.out.println(p.getName());
System.out.println(p.getCategory().getName());
}
}
注解形式
@Component & @Bean
使用 @Component 告知 Spring 在进行组件扫描(@ComponentScan)时需要为哪些类创建 bean
// Spring 将为使用 Component 注解的类创建对象(bean)
// 每一个 Bean 都有一个 ID,默认 ID 为类首字母小写,可以指定,如下 p
@Component("p") // 大部分场景下可以使用 @Named 替换 Component
@Primary // 有歧义时,标识首选bean
public class Product {
...
}
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration// 表名这是一个配置类,包含创建 bean 的细节
@ComponentScan(basePackages={"soundsystem", "video"}) // 组件扫描默认是不开的,需要使用这个注释打开
// @ComponentScan(basePackageClasses={CDPlayer.class, DVDPlayer.class})
// @Import(CDPlayerConfig.class)
// @ImportResource("classpath:cd-config.xml")
public class CDPlayerConfig {
// 默认情况下, bean的ID与带有@Bean注解的方法名是一样的
// 因为 Bean 注解的存在,Spring 会拦截所有下面函数的调用,返回相同的对象实例
@Bean(name="lonelyHeartsClubBand") // 告知 Spring 当前函数返回的对象需要注册为 bean
@Primary
public CompactDisc sgtPeppers() {
return new SgtPeppers();
}
}
自动装配(DI)
// 使用 Autowired 注解,Spring 自动将合适的 bean 绑定到变量
@Autowired // 变量注解,可以使用 @Inject 替换 Autowired
@Qualifier("cc1") // 只有 id 为 cc1 的 bean 才会注入 category 中
private Category category;
@Autowired // 函数注解,Autowired 按照类型进行装配
// @Resource,按照 name 属性进行装配
// @Autowired(required=false),自动装配失败不抛异常
public void setCategory(Category category) {
this.category = category;
}
条件装配
Spring 可以在运行时根据环境变量选择合适的 Beans,这样在测试或上线时就不需要重新构建程序
@Profile
Spring 中可以使用 Profile 注解指定 bean 属于哪个 profile,@Profile 注解可以同时置于方法(Spring 版本需要大于 3.2)和类
@Configuration
@Profile("dev")// 控制配置类生效环境
public class DevelopmentProfileConfig {
@Bean(destroyMethod="shutdown")
public DataSource dataSource() {
...
XML 形式
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
...
profile="dev">
<jdbc:embedded-database id="dataSource">
...
</jdbc:embedded-database>
</beans>
可以在 <beans>
元素中嵌套 <beans>
,并指定不同的 profile
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
...>
<beans profile="dev">
<jdbc:embedded-database id="dataSource">
...
</jdbc:embedded-database>
</beans>
<beans profile="prod">
<jee:jndi-lookup id="dataSource">
...
</beans>
</beans>
激活 profile
Spring 会依次查找 spring.profiles.active
和 spring.profiles.default
这两个变量来确定哪个 profile 处于激活状态,如果二者都未定义,则 Spring 只会创建那些没有定义在 profile 中的 bean。
可以同时激活多个不同的 profile
有很多种方式配置上面两个属性,例如在 web.xml 中
<?xml ...>
<web-app version="2.5" ...>
...
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</context-param>
...
在生产环境可以通过配置 spring.profiles.active
环境变量实现环境切换
@ActiveProfiles
测试环境可直接使用注解激活对应的 profile
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={PersistenceTestConfig.class})
@ActiveProfiles("dev") // 直接激活指定 profile
public class PersistenceTest {
...
}
@Conditional
@Conditional 注解,它可以用到带有 @Bean 注解的方法上。如果给定的条件计算结果为 true,就会创建这个 bean,否则的话, 这个 bean 会被忽略
歧义消除
- @Primary,标识首选 bean
- @Qualifier("iceCream") ,这类方法比较灵活,具体使用时还需查询文档
运行时注入
Spring 提供了两种在运行时求值的方式
- 属性占位符(Property placeholder)
- Spring 表达式语言(SpEL)
@PropertySource & Env
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
@Configuration
@PropertySource("classpath:/xyz/yearn/app.properties")
public class Test{
@Autowired
Environment env;
@Bean
public BlankDisc disc()
{
// getProperty 有多种变种,用时请查 API
return new BlankDisc(env.getProperty("disc.title"), env.getProperty("disc.artist"));
}
}
// app.properties 的内容如下
// disc.title=Sgt.Peppers Lonely Hearts Club Band
// disc.artist=The Beatles
属性占位符 @Value
占位符使用 ${...}
包装属性名称,使用属性占位符需要先配置 PropertySourcesPlaceholderConfigurer
<context:property-placeholder />
<bean id="sgtPeppers" class="soundsystem.BlankDisc" c:_title="${disc.title}" c:_artist="${disc.artist}"/>
@Value
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
public BlankDisc(@Value("${disc.title}") String title,
@Value("${disc.artist}") String artist)
{
this.title = title;
this.artist = artist;
}
SpEL
SpEL 是 Spring 提供的内嵌脚本语言,SpEL 需要放置到 #{...}
之中,示例
#{sgtPeppers.artist}
- SpEL 可以引用 bean 属性
#{T(System).currentTimeMillis()}
&T(java.lang.Math).random()
T()
表达式会将 Java.lang.System 视为 Java 中对应的类型,因此可以调用其 static 方法
#{systemProperties['disc.title']}
- 通过 systemProperties 对象引用系统属性
#{2 * T(java.lang.Math).PI * circle.radius}
和常见脚本语言一样,SpEL 可以计算十分复杂的计算,这部分内容请参考其他文档
面向切面编程(AOP)
AOP (Aspect Oriented Program)即面向切面编程。使用 AOP 可以剥离核心业务与辅助功能代码,减少业务程序员的心智负担:
- 核心业务,比如登录,数据库操作等
- 辅助功能,比如性能统计、日志、事务管理等。AOP 中的切面即为周边业务(或者辅助功能)
下面举个例子
需要添加切面的业务对象
public class ProductService {
public void doSomeService(){
System.out.println("doSomeService");
}
}
切面对象
public class LoggerAspect {
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("start log:" + joinPoint.getSignature().getName());
Object object = joinPoint.proceed();
System.out.println("end log:" + joinPoint.getSignature().getName());
return object;
}
}
关联业务与切面(配置文件)
<aop:config>
<!-- 定义切面位置 -->
<aop:pointcut id="loggerCutpoint" expression="execution(* com.how2java.service.ProductService.*(..)) "/>
<!-- 关联切面和切面对象 -->
<aop:aspect id="logAspect" ref="loggerAspect">
<aop:around pointcut-ref="loggerCutpoint" method="log"/>
</aop:aspect>
</aop:config>
根据 AOP 配置中的 execution,ProductService 中所有方法的调用都将触发 LoggerAspect 切面
注解形式
@Aspect // 这是一个切面对象
@Component
public class LoggerAspect {
// 指明切面的执行位置
@Around(value = "execution(* com.how2java.service.ProductService.*(..))")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("start log:" + joinPoint.getSignature().getName());
Object object = joinPoint.proceed();
System.out.println("end log:" + joinPoint.getSignature().getName());
return object;
}
}