认识Spring
Spring是一个开源框架,目的是为了简化Java开发。
为了降低Java开发的复杂性,Spring采取了以下4种策略:
- 基于POJO的轻量级和最小侵入性编程;
- 通过依赖注入和面向接口实现松耦合;
- 基于切面和惯例进行声明式编程;
- 通过切面和模板减少样板式代码;
POJO
POJO 全称是Plain Ordinary Java Object,翻译过来即普通Java类。普通的一个类为什么要用POJO来称呼那?直接说一个类不就完了嘛。POJO主要用来指代那些没有遵从特定的Java对象模型、约定或框架的Java对象,强调的是不受约束。
依赖注入(DI)
当一个类A中需要用到另一个类B的时,如下面所示:
public class A {
private B b;
public A() {
b = new B();
}
}
这里类A与类B就存在了耦合,为避免这种耦合,我们不应该在类A中创建B的实例,而是交给第三方,把对B的控制权叫出来,所以称之为控制反转(IOC)。那既然类B不是在类A中创建,那么如何才能把类B的实例交给类A那?要么通过构造,要么通过set方法,而这就是依赖注入(DI)。
class A {
private B b;
public A(B b) {
this.b = b;
}
}
依赖注入实现了控制反转,实现了松耦合。但是也导致要写更多的代码,例如我们要是上面的类A,可能需要这样写:
B b = new B();
A a = new A(b);
这是最简单的情况,如果类A依赖的类很多,则需要一个个实例化被依赖的类,然后注入到类A中。而Spring可以帮我们省下这些代码。通过容器管理bean,也即是类A,类B等。
Spring容器
Spring容器可以归纳为两种:BeanFactory和ApplicationContext。通常我们会选择ApplicationContext,它提供了应用框架级别的服务,例如从属性文件解析文本信息,以及发布应用事件。
ApplicationContext有多种实现:
- AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文。
- AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文。
- ClassPathXmlApplicationContext:从类路径下的一个或多个xml配置文件中加载上下文定义,把应用上下文的定义文件作为类资源。
- FileSystemXmlApplicationContext:从文件系统下的一个或多个xml配置文件中加载上下文定义。
- XmlWebApplicationContext:从web应用下的一个或多个xml配置文件中加载上下文定义。
其实区别也就是从不同的地方加载bean的配置文件。
装配bean
Spring容器负责创建应用程序中的bean并通过DI来协调这些对象之间的关系。而我们要做事情则是告诉Spring容器,哪些是bean。我们有三种装配机制可以选择:
- 在xml中进行显示配置;
- 在Java中进行显示配置;
- 隐式的bean发现机制和自动装配;
准备
在装配之前,我们先来准备几个POJO类。
一个cd接口:
public interface CD {
void play();
}
一个cd接口的实现类:
public class SgtPeppers implements CD {
@Override
public void play() {
System.out.println("Playing Sgt. Pepper's Lonely Heart Club Band by The Beatles");
}
}
一个cd播放器用于播放cd:
public class CDPlayer {
private CD cd;
public CDPlayer(CD cd) {
this.cd = cd;
}
public void play() {
cd.play();
}
}
自动装配bean
我们通过@Configuration
注解一个类表示这个类为Spring的配置类。并且通过@ComponentScan
注解开启自动扫描bean:
@Configuration
@ComponentScan
public class AutoConfig {
}
@ComponentScan
默认会扫描与配置类同级以及子级包中所有带有@Component
注解的类,自动创建为一个bean。 我们在需要装配的bean上添加注解:
@Component
public class SgtPeppers implements CD {
...
}
@Component("player")
public class CDPlayer {
...
}
接下来,我们可以AnnotationConfigApplicationContext类加载Spring配置类,看下SgtPeppers是否被自动扫描并创建了bean:
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AutoConfig.class);
System.out.println("----------------------------------------------");
String[] names = applicationContext.getBeanDefinitionNames();
System.out.println(Arrays.toString(names));
System.out.println("----------------------------------------------");
CD cd = (CD) applicationContext.getBean("sgtPeppers");
System.out.println(cd);
CDPlayer cdPlayer = (CDPlayer) applicationContext.getBean("player");
System.out.println(cdPlayer);
cdPlayer.play();
输出的是一个内存地址值,SgtPeppers已经被自动扫描发现,并创建。可以看到我们是通过sgtPeppers来找到类SgtPeppers的bean,这也是默认的id——类名(首字母小写),我们也可以通过@Component("key1")
来手动指定该bean的id为key1 。
你可能注意到CDPlayer的构造需要一个CD类型的参数,Spring会自动查找装配的bean是否有符合该参数的类型,如果发现有则自动传入,如果没有查找到符合类型的bean则会抛出NoSuchBeanDefinitionException异常。
设置组件扫描的基础包
有时候bean和config类可能并不在同级包中的话,那就需要设置扫描的基础包:
@Configuration
@ComponentScan("com.hubert")
public class AutoConfig { }
如果有多个地方需要扫描也可以这样定义:
@Configuration
@ComponentScan(basePackages = {"com.hubert", "music"})
public class AutoConfig { }
除了用String这种硬编码的声明,也可以传入class对象,即将class对象所在的包作为基础包。
@Configuration
@ComponentScan(basePackageClasses = ComponentPackageMaker.class)//扫描自动装载
public class AutoConfig {
这里的ComponentPackageMaker类是一个空接口,用来标识基础包的位置。
目前我们实现了将bean放入Spring容器,除了bean之间构造参数的强制依赖关系会自动注入bean之外,我们也可以通过@Autowired
注解在方法或属性上实现bean的自动注入。例如这里有另一个cd播放器,它通过set方法实现注入:
@Component
public class OtherPlayer {
private CD cd;
@Autowired
public void setCd(CD cd) {
this.cd = cd;
}
public void play() {
cd.play();
}
}
我们可以验证下否正确注入:
OtherPlayer otherPlayer = (OtherPlayer) applicationContext.getBean("otherPlayer");
System.out.println(otherPlayer);
otherPlayer.play();
注意这里与构造参数注入不同,构造是强制的,就算没有添加@Autowired
注解,也必须依赖相对应的bean,而set方法注入如果没有添加@Autowired
注解则不会调用该方法注入,因此不要忘记添加注解。
如果没有匹配到bean,在创建Context的时候Spring会抛出异常。为了避免异常,可以修改为@Autowired(required = false)
表示不是必须的bean,当然这样做之后你就得考虑为null的情况了。
通过Java代码装配bean
这里我们还是一个@Configuration
注解的配置类:
@Configuration
public class JavaConfig {
}
接着可以在配置类中声明bean了:
@Bean
public CD cd() {
return new SgtPeppers();
}
这种方式声明的bean默认id是方法名,这里就是"cd",也可以通过@Bean注解的name属性指定id:
@Bean(name = "myCd")
public CD cd() {
return new SgtPeppers();
}
CDPlayer的构造需要一个CD作为参数,这个时候我们可以把需要依赖的bean设置为方法参数,这样在创建cdPlayer这个bean的时候,容器会去自动查找匹配参数的bean自动装配。
@Bean
public CDPlayer cdPlayer(CD cd) {
return new CDPlayer(cd);
}
同样的,我们可以通过AnnotationConfigApplicationContext加载配置类验证bean的装载情况。
通过xml装配bean
最初的时候xml是Spring配置的主要方式,虽然相比于JavaConfig显得过于繁琐。但在无法在代码中添加@bean
等Spirng注解的时候(如第三方库中),使用xml也是不错的选择。
首先我们需要一个xml文件,并且其中以
元素作为根节点:
xmlns
是命名空间,表示声明xml中使用的标签来源,也方便IDE提示验证xml文件的正确性。
声明一个bean
我们在xml中声明一个cd:
对于有构造参数的bean需要这样声明:
属性注入:
我们使用ClassPathXmlApplicationContext来加载xml配置文件装载bean:
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("my-beans.xml");
System.out.println("----------------------------------------------");
String[] names = applicationContext.getBeanDefinitionNames();
System.out.println(Arrays.toString(names));
System.out.println("----------------------------------------------");
CD cd = (CD) applicationContext.getBean("sgtPeppers");
System.out.println(cd);
CDPlayer cdPlayer = (CDPlayer) applicationContext.getBean("player");
System.out.println(cdPlayer);
cdPlayer.play();
OtherPlayer otherPlayer = (OtherPlayer) applicationContext.getBean("otherPlayer");
System.out.println(otherPlayer);
otherPlayer.play();
Spring3之后引入了c命名空间,来简化构造声明:
启动需要新增了一个命名空间:xmlns:c="http://www.springframework.org/schema/c"
我们通过c:cd-ref="cd"
声明CdPlayer的构造参数,其中c:
是命名空间前缀,cd
是构造参数名,-ref
表示引用类型,="sgtPeppers"
指向id为sgtPeppers的bean。
与c命名空间类似的还有p命名空间,用于简化属性声明:
同样需要新增一个命名空间:xmlns:p="http://www.springframework.org/schema/p"
混合使用
假设我们有两个以上的Spring配置,其中有JavaConfig也有xml配置。我们可以使用import将所有的config归并到一起。
在JavaConfig中可以使用@Import
注解来导入其他配置:
@Configuration
@Import(AutoConfig.class)//导入其他JavaConfig
@ImportResource("my-beans.xml")//导入xml配置
public class JavaConfig {
在xml中同样使用
标签导入其他配置:
注意这里导入Java配置的方式并不是用import标签,而是用bean表示。
不管是使用JavaConfig还是xml进行装配,通常都会创建一个根配置,根配置不装配具体的bean,而是用于组合多个其他配置。
处理自动装配的歧义性
前面我们讲到可以通过@Autowired 注解自动注入对应的bean,但有时候,可能注册了多个相同类型的bean,这时候就会发生歧义,因为Spring容器不知道应该使用哪个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。但是在试图自动装配setDessert
时无法选择唯一的值,会抛出NoUniqueBeanDefinitationException 。
Spring提供了多种可选方案来解决这样的问题:
- 将某一个bean设置为首选(primary);
- 使用限定符(qualifier)缩小bean的范围到只有一个bean;
Primary
我们可以使用@Primary
注解来标记首选bean:
@Component
@Primary
public class Cake implements Dessert {...}
首选消除了歧义性,使得自动装配能够正确执行。需要注意首选标记的唯一性,如果存在有个Dessert实现类的bean都标记了@Primary
,那首选也就失去了作用。
Qualifier
我们也可以使用@Qualifier
来限定注入的bean,下面是直接限定了bean的id:
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
环境与Profile
在开发中通常都会存在不同的环境使用不同的配置,如Database。Spring提供了Profile来指定bean所属的环境,只有相应的环境才会装配该bean。
在Java配置中,可以使用@Profile
注解指定bean所属的环境:
@Configuration
public class DataSourceConfig {
@Bean(destroyMethod = "shutdown")
@Profile("dev")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:scheme.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(DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
}
@Profile
也可以与@Configuration
同时注解Config类,表示该配置类中所有bean都属于该环境。
标明bean所属的环境,接下来就是激活profile。Spring首先读取spring.profiles.active
属性获取指定激活profile,如果没有指定,则使用spring.profiles.default
属性指定的默认profile。如果spring.profiles.default
属性也没有指定,则只装配没有被profile标记的bean。
条件化的bean
假设你希望一个或多个bean只有在应用的类路径下包含特定的库时才创建。这种依赖于某种条件的情况下才装配bean的情形在Spring4之后得到了支持。我们可以使用@Conditional
注解设置条件,如果给定的条件满足则会创建这个bean,否则不会装配。
@Bean
@Conditional(MyCondition.class)
public CD cd() {
return new SgtPeppers();
}
@Conditional
注解需要一个Condition接口的实现类作为参数:
package org.springframework.context.annotation;
import org.springframework.core.type.AnnotatedTypeMetadata;
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}
实现Condition接口并实现matches方法,返回true表示满足条件,返回false表示不满足条件。
我们可以借助ConditionContext判断各种情况:
- 借助
getRegistry()
返回的BeanDefinitionRegistry检查bean定义; - 借助
getBeanFactory()
返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性; - 借助
getEnvironment()
返回的Environment检查环境变量是否存在以及它的值是什么; - 读取并探查
getResourceLoader()
返回的ResourceLoader所加载的资源; - 借助
getClassLoader()
返回的ClassLoader加载并检查类是否存在;
AnnotatedTypeMetadata则能够让我们检查带有@Bean
注解的方法上还有什么其他的注解。
bean的作用域
Spring定义了多种作用域:
- 单例(Singleton):在整个应用中,只创建bean的一个实例。
- 原型(Prototype):每次注入或者通过Spring应用上下文获取的实例,都会创建一个新的bean实例。
- 会话(Session):在Web应用中,为每个会话创建一个bean实例。
- 请求(Request):在Web应用中,为每个请求创建一个bean实例。
默认情况下,Spring应用中所有的bean都是以单例(singleton)的形式创建的。我们可以使用@Scope
注解改变默认作用域:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {...}
ConfigurableBeanFactory.SCOPE_PROTOTYPE的值是字符串"prototype"
, 你也可以直接使用这个字符串,但用常量不容易出现拼写错误。
在Web应用中通常会使用会话和请求范围内共享的bean,例如购物车bean:
@Bean
@Score(value=WebApplicationContext.SCOPE_SESSION,
proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() { ... }
这里我们将ShoppingCart的声明周期设置为session,对于同一个会话只会创建一个ShoppingCart实例。要注意这里还有另一个proxyMode
属性,这个属性解决的是一个短生命周期的bean注入到长生命周期bean中的问题。
假设我们要将ShoppingCart的bean注入到单例StoreService中:
@Component
public class StoreService {
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
}
StoreService是一个单例的bean,当它创建的时候,Spring会试图将ShoppingCart注入到setShoppingCart()方法中。但是ShoppingCart是会话作用域的,此时并不存在,直到某个用户进入系统,创建了会话之后,才会出现ShoppingCart实例。另外,系统中将会存在多个ShoppingCart实例,我们不想让Spring注入某个固定的ShoppingCart实例到StoreService中。我们希望的是当StoreService处理购物车功能时,它所使用的ShoppingCart实例恰好是当前会话所对应的那一个。
所以Spring并不会将实际的ShoppingCart bean注入到StoreService中,Spring会注入到一个ShoppingCart bean的代理。这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。
proxyMode
属性声明了代理的方式,ScopedProxyMode.INTERFACES
表明这个代理要实现ShoppingCart接口。但如果注入的bean是一个类不是接口,Spring就没有办法创建基于接口的代理了。这时候则需要设置proxyMode
属性为ScopedProxyMode.TARGET_CLASS
,以此表明要以生成目标类扩展的方式创建代理。
运行时注入
在Spring中处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。
我们先在resource文件夹中创建一个app.properties
声明属性值,内容是=
链接的键值对。
cd.title=this is cd title
cd.author=hubert
然后在config中通过@PropertySource
注解引入app.properties
。
@Configuration
@PropertySource("app.properties")
public class PropertiesConfig {
private Environment env;
@Autowired
public PropertiesConfig(Environment env) {
this.env = env;
}
@Bean
public BlackDisc disc() {
return new BlackDisc(
env.getProperty("cd.title"),
env.getProperty("cd.author"));
}
}
BlackDisc的构造需要两个String类型的title和author,这里通过Environment的getProperty
方法获取我们在外部声明的属性。getProperty
方法还有几个重载方法,可以传入默认值或者转换目标类型(Class
)。
getProperty
方法在没有传入默认值的情况下,如果属性没有定义,则获取到null。如果你希望该属性是必须的,可以使用getRequiredProperty()
方法。使用该方法获取属性,如果属性没有定义,则会抛出IllegalStateException异常。