文章目录
依赖注入
Spring的核心之一控制反转,即不需要我们来手动创建对象,而是把创建对象的职责交给Spring来去做,我们通过IOC容器来直接获取这个对象。完成这一操作,我们需要做的仅仅是告诉Spring,它需要帮我们创建哪些对象。而有的对象本身内部聚合/组合了其他对象(成员变量),我们也不需要去为这些成员变量赋值,Spring容器负责创建应用程序中的bean并通过DI(依赖注入)来协调这些对象之间的关系,我们需要告诉Spring要创建那些bean并且如何将它们装配到一起,Spring提供了三种主要的装配机制
- 在XML中进行显式的配置
- 在Java中进行显示的配置(JavaConfig)
- 隐式的bean发现机制和自动装配
几乎所有的课件都是基于XML的方式来讲述spring的知识,但是Spring4.0作者推荐使用自动化配置搭配JavaConfig,JavaConfig我真的爱了爱了,这很Java,包括后面的SpringBoot也可以看出来,XML作用会越来越弱化。
装配Bean
自动装配
Spring从两个角度来实现自动化装配
- 组件扫描
Spring会自动发现上下文中创建的bean,即上面说的,我们要告诉Spring,哪些对象是要交给Spring来管理的 - 自动装配
Spring会自动满足bean之间的依赖,从IOC容器中取出对象,自动赋值给需要这个对象的类
@Component
这个注解放在类名上,这个注解表明这个类会作为组件类,Spring会为标注了这个注解的类创建bean并放在IOC容器中。这就是控制反转,创建对象的工作由Spring帮我们完成。bean的ID默认是类名首字母小写,我们可以主动设置Value值来给bean指定ID
@Named
这个注解和@Component的功能用法基本相同,是由Java Dependency Injection提供的
@Component(value = "beanID")
public class Human{}
@Named(value = "beanID")
public class Teacher{}
@ComponentScan
这个注解用于开启组件扫描,默认会扫描与配置类相同的包以及这个包下面的子包。可以通过指定value属性的值来扫描指定的包
//通过设置value值来指定要扫描的包及其子包
@ComponentScan("soundsystem")
//也可以通过basePackages属性来设置
@ComponentScan(basePackages="soundsystem")
//basePackages可以设置扫描多个包,这时候传入一个数组
@ComponentScan(basePackages={"soundsystem","video"})
//上述的方式都是将包名以String的方式传入,这种方式是类型不安全的,重构代码会导致包名改变
//可以将其指定为包中所含的类或接口,指定的类所在的包会作为扫描的基础包
//建议在包中创建一个用来进行扫描的空接口,因为重构可能会导致应用代码从想要扫描的包中移除掉
@ComponentScan(basePackageClasses={"CDplayer.class","DVDPlayer.class"})
<!--开启自动扫描-->
<context:component-scan base-package="包名">
自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean的其他bean,我们使用@AutoWired来声明要进行自动装配
@AutoWired
public class CDPlayer implements MediaPlayer{
//直接放在成员变量上
@AutoWired
private CompactDisc cd;
//构造器注入
@AutoWired
public CDPlayer(CopmactDisc cd){
this.cd=cd;
}
//属性注入
//将安全检查设为false,允许找不到装配的bean,默认为true,没有匹配的bean会抛出异常
@AutoWired(required=false)
public void setCompactDisc(CopmactDisc cd){
this.cd=cd;
}
}
@Inject
这个注解来源于Java依赖注入规范,和@AutoWired基本相同
不管构造器,Setter方法还是其他方法,Spring都会尝试满足方法参数上声明的依赖。假如有且仅有一个bean匹配依赖需求的话,那么这个bean将会被装配进来,但如果没有匹配的bean,那么会抛出异常,为了避免异常的出现,可以把@AtuoWired的required的属性设置为false。但如果有多个匹配的bean呢?在后面我们会单独讲述自动装配的歧义性
显式装配
尽管组件扫描和自动装配来实现Spring的自动化配置是推荐方式,但有的时候,我们没办法在一个类上面通过添加@Component和@AutoWired注解(比如第三方组件)来将这些组件装配到工程中,这种情况我们就需要显示装配
通过Java代码装配
JavaConfig是我很喜欢的装配方案,相对于自动装配,显示装配显得更有据可循和信任,类型安全且易于重构,而跟XML相比,JavaConfig显得更加易于理解和可读,并且JavaConfig也是Java代码,这种一致性我是非常钟意的。JavaConfig是配置代码,不包因那个该包含任何的业务逻辑,我们通常会将JavaConfig放到单独的包中,与其他应用的逻辑分离开。
//这个注解用来表明这个类是一个配置类
@Configuration
public class CDPlayerConfig{
//@Bean注解会告诉Spring这个方法将返回一个对象,该对象要注册为Spring应用上下文的bean。
//方法体重包含了最终产生bean的实例逻辑,只要最后可以返回一个bean就可以,所以我们可以在这个方法里面干很多事情
//默认情况bean的ID与带有@Bean的注解的方法名是一样的,可以通过name属性来指定ID
@Bean(name="sgtPaperBean")
public CompactDisc sgtPeppers(){
return new SgtPeppers();
}
/**
*方法请求一个CompactDisc作为参数,当Spring调用这个方法来创建对象的时候,它会自动装配一个ComoactDisc对象到配置中去
*无论这个对象是谁创建的,只要它存在在ioc容器中就可以
*/
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc){
return new CDPlayer(compactDisc);
}
/*
我们还可以这样进行配置,因为sgtPeppers方法被@Bean标记了,所以所有访问这个方法都会被拦截,并返回一个IOC容器中已有的对象,并不会调用一次就生成一个对象
*/
@Bean
public CDPlayer cdPlayer(){
return new CDPlayer(sgtPeppers());
}
}
@Configuration标记的类必须符合下面的要求
- 配置类必须以类的形式提供(不能是工厂方法返回的实例),允许通过生成子类在运行时增强(CGLIB动态代理)
- 配置类不能是final类(没法动态代理)
- 配置注解通常为了通过@Bean注解生成Spring容器管理的类
- 配置类必须是非私有的(即不能在方法中声明,不能是 private)
- 任何嵌套配置类都必须声明为static
- @Bean 方法不可能会反过来创建进一步的配置类,也就是返回的bean如果带有@Configuration,也不会被特殊处理,只会作为普通的 bean
关于JavaConfig日后再来填坑
通过XML装配Bean
麻烦的很,尤其是依赖注入这块。DI还是推荐使用注解吧!
<!-- 配置一个 bean -->
<!-- id就是IOC容器中bean的ID,class为bean的全路径名(反射创建对象) -->
<bean id="helloWorld" class="com.HelllowSpring.helloworld.HelloWorld">
<!-- 为属性赋值 -->
<!-- 通过属性注入: 通过 setter 方法注入属性值 -->
<property name="user" value="Tom"></property>
</bean>
<!-- 通过构造器注入属性值 -->
<bean id="helloWorld1" class="com.HelllowSpring.helloworld.HelloWorld">
<!-- 要求:在Bean中必须有对应的构造器. -->
<constructor-arg value="Mike"></constructor-arg>
</bean>
<!-- 通过 ref 属性值指定当前属性指向哪一个bean -->
<bean id="daoRef" class="com.HelllowSpring.helloworld.Dao"></bean>
<bean id="service" class="com.HelllowSpring.helloworld.Service">
<property name="dao" ref="daoRef"></property>
</bean>
<!-- 声明使用内部 bean -->
<!-- 内部 bean, 类似于匿名内部类对象。不能被外部的bean来引用,,所以没有必要设置id属性 -->
<bean id="service2" class="com.HelllowSpring.helloworld.Service">
<property name="dao">
<bean class="com.HelllowSpring.helloworld.Dao">
<property name="dataSource" value="c3p0"></property>
</bean>
</property>
</bean>
<!-- 装配集合属性 -->
<bean id="user" class="com.HelllowSpring.helloworld.User">
<property name="userName" value="Jack"></property>
<property name="cars">
<!-- 使用 list 元素来装配集合属性 -->
<list>
<ref bean="car"/>
<ref bean="car1"/>
</list>
</property>
</bean>
<!-- 声明集合类型的 bean -->
<util:list id="cars">
<ref bean="car"/>
<ref bean="car1"/>
</util:list>
<!-- 引用外部声明的 list -->
<bean id="user2" class="com.HelllowSpring.helloworld.User">
<property name="userName" value="Rose"></property>
<property name="cars" ref="cars"></property>
</bean>
混合配置
Spring支持混合配置,即自动装配、XML和javaConfig可以混合在一起使用。在自动装配的时候,它并不在意要装配的bean来自哪里,自动装配会考虑到Spring容器中的所用bean,不管它是在JavaConfig或XML中声明的还是通过组件扫描获取到的
- 在JavaConfig中引入其他配置
@import 引入配置类
@importResource引入XML文件 - 在XML中引入其他配置
JavaConfig本质上也是一个Java类,所以我们直接在XML文件中以标签引入这个类就可以了
环境与Profile
@Profile
指定某个bean属于哪个profile,只有指定的profile处于激活状态,那么这个bean才会被创建。
- 这个注解可以添加在配置类上。如果对应的配置文件没有被激活,那么这个配置类中所有带有@Bean注解的方法都会被忽略掉。
- 如果注解添加到方法上,那么这个方法只有规定的profile被激活了才会被创建。同类下其它没有被@Profile标记的bean方法始终都会创建,与激活哪个profile没有关系
@Configuration
public class CDPlayerConfig{
//只有名为dev的配置文件(配置类)处于激活状态这个bean才会被Spring创建
@Profile("dev")
@Bean(name="sgtPaperBean")
public CompactDisc sgtPeppers(){
return new SgtPeppers();
}
//因为@Profile注解只是加在了方法上,所以这个类无论如何都会被Spring创建
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc){
return new CDPlayer(compactDisc);
}
}
在XML中我们通过beans标签的profile属性来声明这个beans属于哪个profile
<beans profile="QA">
<bean></bean>
</beans>
Spring在确定哪个profile处于激活状态时,需要依赖两个独立属性:
- srping.profiles.active
- spring.profiles.default
如果设置了active属性的话,那么它的值就用来确定哪个proflie是激活的。如果没有设置的话,Spring会查找default的值,如果都没有设置的话,那就没有激活proflie,就只会创建没有定义在profile中的bean。可以同时激活多个profile,以逗号分隔。
有多种方式来设置这两个属性
- 作为DispatcherServlet的初始化参数
- 作为Web应用的上下文参数
- 作为JNDI条目
- 作为环境变量
- 作为JVM的系统属性
- 在集成测试类上,使用@ActiveProfiles注解
条件化的Bean
@Conditional
这个注解用在带有@Bean注解的方法上,如果给定条件为TRUE,就会创建这个bean,否则的话这个bean会被忽略,不会被创建
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean(){
return new MagicBean();
}
设置给@Conditional的类可以是任意实现了Condition接口的类
public interface Condition{
boolean matches(ConditionContext ctxt,AnnotatedTypeMetadata metadata);
}
public class MagicExistsCondition implements Condition{
public boolean matches(ConditionContext context,AnnotatedTypeMetadata metadata){
Environment env = context.getEnvironment();
return env.containsProperty("magic");
}
}
在上面的程序中,方法通过给定的ConditionContext对象得到了Environment对象,并用这个对象来检查环境中是否存在名为magic的环境属性。通过ConditionContext我们可以做到如下几点
- 借助getRegistry() 返回的BeanDefinitionRegistry 检查bean的定义
- 借助getBeanFactory() 返回的ConfigurableListableBeanFactory 检查bean是否存在,或者来探查bean的属性
- 借助getEnvironment() 返回的Enironment 检查环境变量是否存在以及它们的值是什么
- 读取并探查getResourceLoader() 返回的ResourceLoader 所加载的资源
- 借助getClassLoader() 返回的ClassLoader 加载并检查类是否存在
处理自动装配的歧义性
- 解决方案一 @primary
我们可以把多个符合匹配条件的bean中通过设置首选bean
@Bean
@Primary
public Dessert iceCream(){
return new IceCream();
}
<bean id="iceCream" class="com.desserteater.IceCream" primary="true" />
- 解决方案二 @Qualifier
但如果有多个bean被设置了首选bean,我们可以使用限定符的方式
@Qualifier注解是使用限定符的主要方式
@Autowired
//这里只会注入同样标注了@Qualifier("iceCream")的bean
@Qualifier("iceCream")的bean
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
这是使用限定符最简单的例子,@Qualifier的参数就是想要注入的bean的ID,所有使用@Component注解声明的类都会创建为bean,并且bean的ID为首字母小写的类名,实际上@Qualifier(“iceCream”)所指向的bean要有一个String 的“iceCream”作为限定符,所有的bean都有一个默认的限定符,这个限定符与bean的ID相同,但这种情况限定符与bean的ID紧紧耦合在一起,如果我们修改了bean的名称就会导致限定符的失败,所以我们可以限定符设置自定义值
@Component
//当使用自定义值的时候,建议为bean选择特征性活描述性的词语,而不是随意的名字
@Qualifier("cold")
public class IceCream implements Dessert{}
有一种情况,当不同的类都采用了同样的描述性的注解,这时候也会有歧义性,我们可能会想到使用多个@Qualifier注解来将bean限定到一个,但遗憾的是,@Qualifier注解并没有在定义时添加@Repeatable注解,所以Java不允许在同一个位置上重复出现多个@Qualifier注解
,但是我们可以创建自定义的限定符注解,这个注解本身使用@Qualifier注解来标注
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.ANNOTATION_TYPE.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {}
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.ANNOTATION_TYPE.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy {}
//这样,我们在声明和注入bean的地方就可以同时加上这两个注解,从而将可选范围缩小到只有一个bean满足
@Component
@Cold
@Creamy
public class IceCream implements Dessert{}
@Autowired
@Cold
@Creamy
public void setDessert(){
this.dessert = dessert;
}
bean的作用域
- 单例(Singleton)
在整个应用中,只创建bean的一个实例 - 原型(Prototype)
每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例 - 会话(Session)
在Web应用中,为每个会话创建一个bean实例 - 请求(Request)
在Web应用中,为每一个请求创建一个bean实例
单例是默认作用域,使用@Scope注解来声明bean的作用域,这个注解可以和@Component和@Bean搭配使用。也可以在xml中,使用bean的scope属性来设置
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad{}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad{
return new Notepad();
}
<bean id="notepad" class="com.myapp.Notepad" scope="prototype" />
使用请求和会话作用域
一个生活场景,购物车bean,这个bean应该是作为会话作用域,在整个会话和请求范围内共享数据。
@Component
@Scope(value=WebApolicationContext.SCOPE_SESSION,
proxyMode=ScopedProxyMode.INTERFACES
)
public ShoppingCart cart(){
}
@Component
public class StoreService{
@AutoWired
public void setShoppingCart(ShoppingCart shoppingCart){
this.shoppingCart=shoppingCary;
}
}
我们创建了两个对象,一个是会话作用域的ShoppingCart,一个是单例的StoreService,StoreService中聚合了ShoppingCart对象。但这样会有一个问题,单例对象会在加载时创建,而这时候成员变量的对象还没有生成,只有创建了会话之后才会出现这个实例,并且我们希望StoreService对象里面的这个ShoppingCart是针对当前会话的,不同会话中的这个对象是不一样的。为了解决这个问题,我们在ShoppingCart中设置了proxyMode属性,这个属性会为ShoppingCart对象生成一个代理对象,当调用的时候,代理对象会将请求转发给会话作用域里真正的ShoppingCart
proxy的属性有两种
- ScopedProxyMode.INTERFACES
这个是基于JDK实现的 - ScopedProxyMode.TARGET_CLASS
这个是基于CGLIB实现的
运行时值注入
Spring提供了两种运行时求值的方式
- 属性占位符
- Spring表达式语言
注入外部的值
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
public class ExpressiveConfig{
@Autowired
Environment env
@Bean
public BlankDisc disc(){
return new BlankDisc(env.getProperty("disc.title"),env.getProperty("disc.artist"));
}
}
上面的例子中,@PropertySource引用了类路径中一个名为app.properties的配置文件,这个文件会被加载到Environment中,通过Environment的getProperty方法,我们可以获取到配置文件中对应的值
Environment
- Environment对象的getProperty方法有四个重载
- String getProperty(String key)
- String getProperty(String key, String defaultValue)
- T getProperty(String key, Class type)
- T getProperty(String key, Class type, T defaultValue)
前两个方法的返回值都是String值,不同的是,如果在指定的属性不存在的时候,我们可以通过设置defaultValue给他返回一个默认值。后两个方法我们可以把配置文件里面的值装换成我们需要的类型
//返回值是一个int类型,并且如果配置文件中没有对应的属性,设置默认值30
int connectionCount=env.getProperty("db.connection.count",Integer.class,30)
- getRequiredProperty()
如果使用getProperty()方法的时候没有指定默认值,并且这个属性没有定义的话,获取到的值是null,如果这个属性必须定义的话,个可以使用getRequiredProperty()方法,如果这个方法获取的属性没有定义会抛出异常。 - containsProperty()
使用containsProperty()方法来检查某个属性是否存在,使用getPropertyAsClass()方法将属性解析为类 - 检查哪些profile处于激活状态
- String [] getActiveProfiles() 返回激活profile名称的数组
- String [] getDefaultProfiles() 返回默认profile名称的数组
- boolean acceptsProfiles(String…profiles) 如果environment支持给定profile的话就返回true
属性占位符
Spring支持将属性定义到外部的属性的文件中,并使用占位符将其插入到SpringBean中,占位符的形式为使用"${…}"包装的属性名称
public BlankDisc(
@Value("${disc.title}") String title ,
@Value("${disc.artist}") String artist) {
this.title=title;
this.artist=artist;
}
为了使用占位符,我们需要配置一个PropertyPlaceholderConfigurerbean 或 PropertySourcesPlaceholderConfigurerbean,推荐使用后面的,以为它可以基于SpringEnvironment及其属性源来解析占位符
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
return new PropertySourcesPlaceholderConfigurer()
}
<context : property-placeholder />
Spring表达式语言
使用Spring表达式语言进行装配,SpEL表达式要放到"#{…}"
SpEL的常见特性
- 使用bean的ID来引用bean
- 调用方法和访问对象的属性
- 对值进行算数、关系和逻辑运算
- 正则表达式匹配
- 集合操作
- #{1}
固定值 - #{T(System).currentTimeMillis()}
调用静态方法获取返回值,SpEL中访问类作作用域的方法(静态方法)和常量的话,要依赖 T() 这个运算符 - #{sgtPeppers.artist}
获取ID为sgtPeppers的bean的artist属性 - #{sgtPeppers.selectArtist()}
获取ID为sgtPeppers的bean的selectArtist()方法的返回值,对于被调用方法的返回值来说,我们同样可以调用它的方法,比如这样
#{sgtPeppers.selectArtist().toUpprerCase()},这样写可能会出现空指针异常,我们可以在这里面写Java代码来规避它,但SpEL提供了更简单的方式#{sgtPeppers.selectArtist()?.toUpprerCase()},"?." 会先判断它左边的对象是不是null,如果不是则继续调用右边的方法,否则返回一个null - #{systemProperties[‘disc.title’]}
引用配置文件中的属性
SpEL运算符
运算符类型 | 运算符 |
---|---|
算术运算 | +,-,*,/,%,^ |
比较运算 | <,>,==,<=,>=,lt,gt,eq,le,ge |
逻辑运算 | and,or,not,* |
条件运算 | ?:(ternary),?:(Elvis) |
正则表达式 | matches |
条件运算符与Java中的三元运算符很相似
一种用法是?前面是一个boolean值,ture的时候整个表达式的值是:之前的值,否则是:之后的值,这与Java中完全一致。还有一种用法是用来判断null,#{disc.title ? : ‘JackSon’} 整个表达式会判断disc.title的值是不是null,如果是的话,则表达式的结果就是JackSon
正则表达式example
#{admin.email matches ‘[a-zA-Z0-9._%±]+@[a-zA-Z0-9.-]+//.com’}
计算集合
- #{jukebox.songs[4].title}
获取集合songs中的第五个(从零开始)元素的title属性,整个集合来自ID为jukebox的bean - #{‘abcd’[2]}
还可以从String中获取一个字符,上面的表达式的值就是"c"
SpEL还提供了
- (.?[])
查询运算符 (.?[]) 用来对集合进行过滤,[]里面的是一个另一个表达式,值为boolean值,用于过滤逻辑。相当于Stream里面的filter - (.1)
返回满足过滤条件的第一个匹配项 - (.$[])
返回满足过滤条件的最后一个匹配项 - (.![])
投影运算符,它会从集合的每一个成员中选择特定的属性放到另外一个集合中,相当于Stream里面的map