第一部分 Spring核心
第一章 Spring之旅
Spring 来自于《Export One on One :J2EE Design and Development》这本书,初创者是Rod Johnson
Spring 采取了以下4种策略
- 基于POJO的轻量级和最小入侵性编程;
- 通过一来注入和面相接口实现松耦合
- 通过切面和管理进行申明式编程;
- 通过切面和模板减少样板式代码。
依赖注入
- 构造器注入, constructor injection
对依赖进行替换的一种最常用的方法就是使用mock对象
装配
创建应用组件之间协作的行为叫做装配(wiring)
有两种方式进行装配:
- xml
- java注解
- @Configuration
- @Bean
加载装配的配置文件
Application Context
ClassPathXmlApplicationContext
应用切面
apspect-oriented programming AOP
允许你把遍布应用各处的功能分离出来形成可重用的组件。
AOP的常见栗子为i日志、事务管理、安全等系统服务。这些一般被叫做横切关注点。
AOP把这些服务模块化,并以声明的方式应用到他们能需要影响的组件中。
AOP确保了POJO的简单性。
老式的、基于xml配置文件的AOP其实也挺烦的,因为他和代码分离,当这种代码很多的时候,这样调试起来其实很麻烦
Spring 容器
用来创建、装配并管理Bean的整个生命周期。
有两种容器:
- bean工厂
- 应用上下文
相对来说,应用上下文更加受欢迎,也用的更多。
应用上下文
有多种:
- AnnotationConfigApplicationContext
- AnnnotationConfigWebApplicationContext;
- ClassPathXmlApplicationContext;
- FileSystemXmlApplicationContext;
- XmlWebApplicationContext;
Bean生命周期
主要是有一些勾子
- BeanNameAware
- BeanFactoryAware
- ApplicationContextAware
- BeanPostProcessor
- InitializingBean
- BeanPostProcessor
- DisposableBean
估计大部分时候,bean都不会去实现这些接口,即使有,也是偶尔实现个吧。
Spring体系结构
新的spring有变化了,老的就不看了。
第二章,装配Bean
Spring配置的可选方案
- 在xml中装配;
- 在java中装配;
- 自动装配;
尽可能的去使用自动装配》然后是基于java的装配》最后才是xml装配。
因为JAVA装配具备类型安全,所以优于xml装配
自动化装配Bean
通过两个角度来实现:
- 组件扫描,Spring会自动发现上下文中所创建的bean;
- 自动装配,Spring自动满足bean之间的依赖;
创建可被发现的bean
- @Component
- @Named,这个基本等效于 @Component,一般不用。
- @Configuration
- @ComponentScan
@Configuration
@ComponentScan
public class CDPlayerConfig{
}
自动创建的Bean的命令,就是把类的第一个字母小写,如果想要其他的名字,则只要传递给@Component注解即可。
设置组件扫描的基础包
@ComponentScan("soundsystem")
public class CDPlayerConfig{}
@ComponentScan(basePackages="soundsystem")
public class CDPlayerConfig{}
@ComponentScan(basePackages={"soundsystem", "video"})
public class CDPlayerConfig{}
@ComponentScan(basePackagesClasses={CDPlayer.class, DVDPlayer.class})
public class CDPlayerConfig{}
以上这些都是可以到,要注意其中的区别,其中最后一个应该相对好一点,具备类型安全。
而且你可以不用业务类,而是在每个package中专门增加一个类用于自动扫描用。
添加注解实现自动装配
- @Autowired
- 类似的还有一个 @Injected,一般也不用。
他可以用在构造器,或者setter上,其实他可以用在任何方法上。
如果设置了@Autowired,但是在装配的时候又找不到对应的bean,则Spring会抛出一个异常。
要避免的话,可以设置 @Autowired的required属性为false
@Autowired(required=false)
public void setCD(...){ ... }
如果有多个bean可以满足依赖关系,Spring也会抛出异常,此时要解决装配的歧义性。这点以后再说
通过java代码装备bean
自动装配有失效的时候,比如需要讲第三方的代码装配到你的应用中,此时@Component, @Autowired就无效了。
选择JavaConfig进行装配,有几个优点:
- 更加强大
- 类型安全
- 对重构友好
Javaconfig是配置代码,不应该包含任何业务逻辑。
通常应该将Javaconfig放到单独的包中,使他与其他应用程序逻辑分离,这样对于他的意图就不会产生困惑了
创建配置类
@Configuration
@Bean ,会告诉Spring这个方法会返回一个对象,该对象要注册位Spring应用上下文中的bean,默认情况下,bean的id和这个方法的名称相同。如果想要不一样,则给@Bean指定name参数。
借助Javaconfig实现注入
@Bean
public CDPlayer cdPlayer(){
return new CDPlayer(sgtPepers())
}
看起来,cdPlayer()是通过调用sgtPepers()得到的,但情况并非完全如此,因为sgtPepers()方法上添加了@Bean注解,Spring将会拦截所有对他的调用,并确保直接返回该方法锁创建的bean,而不是每次都对其进行实际的调用。
就是说,实际上只创建了一个SgtPepers对象,然后保存在Spring容器中,以后每次都返回这个对象。
所以,默认情况下,Spring中的bean都是单例的。
上面的装配代码让人有点困惑,下面的更好理解点
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc){
return new CDPlayer(compactDisc)
}
然后Spring会自动传入compactDisc bean,不需要手动指定。这样就不要求 sgtPepers()方法和 cdPlayer()在一个配置类中了,甚至不需要在JavaConfig中声明,他可以是自动扫描发现或者xml文件中配置的。
通过xml装配bean
虽然xml配置已经部署主流,但是历史遗留代码中有大量的xml配置,所以理解xml配置还是必要的。
创建xml配置规范
可以使用Spring Tool Suite来创建spring的xml配置文件,会方便点。
根元素是 <beans>
声明一个简单 <bean>
<bean>元素类似于JavaConfig中的@bean注解。
Spring发现这个bean元素时,他将会调用他的默认构造函数来创建bean,所以相对来说没有JavaConfig灵活。
由于在xml中是以字符串的方式来指定类的名称,所以没有编译器的类型合法检查。
可以使用Spring Tools Suite IDE来检查xml配置的合法性。不然到时候有的问题不好排查。
借助构造器注入初始化bean
- 子元素 <constructor-arg>
<bean id="cdPlayer" class="soundsystem.CDPlayer">
<constructor-arg ref="conpactDisc"/>
</bean>
- c-命名空间
例如:
<bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" >
这里:cd中的cd就是参数名称,在xml中指定参数名称不太好,万一重构时改了呢,所以有一种替代方案,用下划线占位,这种格式只适合有一个参数的情况。
<bean id="cdPlayer" class="soundsystem.CDPlayer" c:_-ref="compactDisc" >
将字面量注入到bean中
<bean id="compactDisc" class="soundsystem.BlankDisc">
<consturctor-arg value="三毛流浪记" />
<consturctor-arg value="三毛" />
</bean>
和之前传入bean时使用ref不同,这里使用了value
装配集合
这种时候只能使用<constructor-arg>,而不能使用c:命令空间
<constructor-arg>
<list>
<value>haha, where are you 1</value>
<value>haha, where are you 2</value>
</list>
</constructor-arg>
这里list的子元素是value,其实也可以是ref
list元素也可以用set元素替代,只不过set不允许重复,赋值之后会过滤重复项
设置属性
一般对于强依赖使用构造器注入,对于可选依赖使用属性注入。
<bean id="cdPlayer" class="soundsystem.CDPlayer">
<property name="compactDisc" ref="compactDisc"/>
</bean>
通过ref引用另外一个bean,如果是字面量,则使用value
与构造函数可以通过c-命名空间来简化传参类似,可以使用p-命名空间来简化属性的传参。
<bean id="cdPlayer" class="soundsystem.CDPlayer" p:compactDisc-ref="compactDisc"/>
与构造函数中类似,给属性传参时,也可以传递数组,此时不能使用p-命名空间,而是使用property。
定义变量
可以使用util-命名空间的工具来定义变量,比如:
<util:list id="trackList">
<value>哈哈,你在哪?1</value>
<value>哈哈,你在哪?2</value>
<value>哈哈,你在哪?3</value>
</util:list>
然后就可以通过p-命名空间在bean定义中引用他。
最后还有一点,p-命名空间可以和<property>混用,也就是一部分属性使用p-,一部分使用property
util中可用的工具包括
- util:constant
- util:list
- util:map
- util:properties
- util:property-path
- util:set
###导入与混合配置
首先我们需要明白,自动装配,javaconfig以及xml配置,可以混用。他们有各自的优缺点,我们可以把他们混合,发挥各自的优点。
自动装配时,Spring其实不在于bean来自于哪里,他会考虑所有的bean,在容器中就行。
在JavaConfig中引用xml配置
如果JavaConfig拆分为多个类,则使用 @Import注解。
如果要在JavaConfig中引用xml定义的配置,则使用 @ImportResource(“classpath:cd-config.xml”)
在xml中引入Javaconfig定义的bean
在xml配置中,可以使用<import>来拆分配置,这里import引入了其他的xml配置文件。
<import resoune="cd-config.xml"/>
如果要引入JavaConfig中的配置,则可以
<bean class="soundsystem.CDConfig"/>
惯例
一般会创建一个根配置,引入其他的所有子配置,并且在根配置中启动组件的自动扫描。如果在xml中,就是用 <context:component-scan>,如果在Javaconfig中,则使用 @ComponentScan
第三章 高级装配
环境与profile
比如我们在开发、测试、QA、生产等不同环境下的数据源的不同,此时就可以用profile注解。
Spring引入了bean profile功能,于是有了 @Profile注解,这个注解可以用到类中,也可以用到方法中。
例如:
@Configuration
@Profile("dev")
public class DevelopmentProfileConfig{}
没有指定@Profile的bean始终都会创建。
在xml中配置profile
<beans profile="dev">
<jdbc:embedded-database id="datasource">
...
</jdbc:embedded-database>
</beans>
激活profile
依赖于两个独立的属性:
- spring.profiles.active
- spring.profiles.default
其中active优先,如果两个都没设置,则所有Profile的bean都不会创建。
可以配置上面这两个属性的方式:
- 作为DispatcherServlet的初始化参数;
- 作为Web应用上下文的参数;
- 作为JNDI条目;
- 作为环境变量;
- 作为JVM的系统属性;
- 在集成测试类上,使用 @ActiveProfiles注解设置。
可以同时激活多个profile,虽然一般没有必要。
集成测试的时候使用@ActiveProfile
@ActiveProfile("dev")
public class PersistenceTest{}
条件化的bean
@Conditional注解,他可以用到带有@Bean注解的方法上,如果给定的条件是真就创建bean,否则就不创建。
这种特殊的条件,可以是:
- 路径下包含特定的库;
- 另外一个什么bean存在;
- 设定了某个特定的环境变量;
- 。。。
其实可以是任何可以通过编程判断的条件。
@Conditionnal注解接受的参数是一个实现了Condition接口的类,定义如下
public interface Condition{
boolean matches(ConditionContext ctxt, AnnotatedTypeMetadata metadata)
}
使用的时候是这样
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean(){
return new MagicBean()
}
再来说说这个Condition对接口 matches的参数
ConditionContext是一个接口,定义这些方法
public interface ConditionContext{
BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Enviroment getEnviroment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader();
}
而AnnotatedTypeMetadata也是一个接口,通过他可以拿到当前bean还有哪些注解,一些这些注解的属性,以便做进一步的判断。
处理自动装配的歧义性
仅有一个bean的时候,自动装配才会生效。
有几种方法可以解决:
标示首选的bean
使用@Primary注解,和@Component一起使用,也可以和@Bean注解一起使用。
@Component
@Primary
public class IceCream implements Dessert{}
//或者
@Bean
@Primary
public Dessert iceCream(){
return new IceCream()
}
使用限定符
使用 @Qualifier注解设置一个限定符,可以和@Autowired, @Injected一起使用。
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){}
这里的iceCream就是bean的id,其实这个iceCream是一个针对bean的限定符,只不过一般都不设置限定符,此时限定符的默认值就是id值
创建自定义的限定符
可以给bean创建自定义的限定符,这样在引用的时候就不会和类名强耦合了,例如
@Componet
@Qualifier("cold")
public class IceCream implements Dessert{}
这里是和bean类的定义写一起了,其实这个@Qualifier也可以和@Bean的方法写一起
@Bean
@Qualifier("cold")
public Dessert iceCream(){}
使用自定义的限定符注解
创建一个新的注解,只要该注解使用@Qualifier注解来限定,那么新的注解也就具有了 @Qualifier注解的特性了。
@Target(ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @inerface Cold{}
这样,我们就定义了一个@Cold注解
其实作者写这个,只是为了绕开一个位置不能有多个@Qualifier注解的问题,感觉必要性不是很大。
bean的作用域
在有些任务中,让对象保持屋状态并且在应用中反复重复用这些对象是不合理的。有些类是易变的(mutable)。
Spring有多种作用域:
- 单利,singleton;这个是默认的。
- 原型,prototype,每次都是新的;
- 会话,session,在一个session内是新的;
- 请求,request,在一个request内是新的;
这些都能理解,和web的一些对象的生命周期差不多。
可以使用@Scope注解来修改,比如:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
//@Scope('prototype'),这样也是可以的,这是这种不具备类型安全
public class Notepad{}
这个scope也可以和@Bean一起使用。
使用会话和请求作用域
看个栗子
@Bean
@Scope(value=WebApplicationContext.SCOPE_SESSION, proxyMode=ScopeProxyMode.INTERFACE)
public ShoppingCart cart(){ ... }
要注意下这里的proxyMode,这是为了解决爸scope为session的bean装配被scope为singleton的类时可能会遇到的问题。
Spring会创建一个代理,给singleton的bean装配session的bean时,不会装配具体的bean,而是装配一个和bean实现了相同接口的代理,然后代理内部会根据具体的session去调用各自session内的bean。
有点复杂,看书上p87的那个图就明白了。
这个代理叫做作用域代理这个代理是通过CGLib
来创建的。
在xml中声明作用域代理
其实就是使用了aop命名空间的一个新元素
<aop:scoped-proxy/>
运行时值注入
Spring提供了两种在运行时秋实的方式:
- 属性占位符
- Spring表达式语言, SpEL
注入外部的值
在Spring中,处理外部值的最简单的方式就是生命属性源并通过Spring的Environment来检索属性。
例如:
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
public class ExpresiveConfig{
@Autowired
Enviroment env;
@Bean
public BlankDisc disc(){
return new BlankDisc(env.getProperty("disc.title"), env.getProperty("disc.artist"))
}
}
disc.title=你好啊
disc.artist=许三多
看到这里,我就有点明白各种数据源的配置了
深入学习Spring的Enviroment
getProperty的4个overload
- String getProperty(String key);
- String getProperty(String key, String defaultValue)
- T getProperty(String key, Class type);
- T getProperty(Stirng key, Class type, T defaultValue)
其他几个需要了解的方法
- String getRequiredProperty(String key),值不存在的时候抛出异常
- boolean containsProperty(String key),判断是否存在
- T getPropertyAsClass(String key, Class type),将获取的值转换为class
- String[] getActiveProfiles()
- String[] getDefaultProfiles()
- boolean acceptsProfiles(String … profiles)
后面三个不知道是静态方法还是普通方法?
解析属性占位符
占位符的格式:
${key}
这个占位符可以在xml,
<bean id="sgtPepers" class="soundsystem.BlankDisc" c:_title="${disc.title}" c:_artist="${disc.artist}"/>
也可以在Javaconfig中使用
public BlankDisc(@Value("${disc.title}") String title, @Value("${disc.artist}") String artist){
this.title = title
this.artist = artist
}
注意这里用到了 @Value注解
然后还需要一个placeholder来配合
javaconfig的配置
@Bean
public PropertySourcesPlaceHolderConfigurer placeholderConfigurer(){}
xml的方式
<context:property-placeholder/?
总的来说,解析外部属性,能够将值的处理推迟到运行时。
使用Spring表达式预言进行装配
Spring表达式要放在 #{…}内部,下面给一些栗子
#{1} // 常量
#{T(System).currentTimeMillis()} //调用对象方法
#{sgtPepers.artist} // 调用bean的属性
#{systemPropertis['disc.title']} //读取propertis
这些表达式可以在Javaconfig中使用,也可以在xml配置中使用。
标示字面值
#{3.1415926}
#{false}
#{‘Hello’}
引用bean、属性和方法
- #{sgtPeppers}
- #{sgtPepers.artist}
- #{artistSelector.selectArtist()}
- #{artistSelector.selectArtist().toUpperCase()}
- #{artistSelector.selectArtist()?.toUpperCase()}
在表达式中使用类型
要使用T(…)的形式,比如
- #{T(java.lang.Math).PI}
- #{T(java.lang.Math).random()}
SpEL运算符
基本上支持常用的算数、逻辑、比较、条件运算符,以及正则表达式
比如
- #{2T(java.lang.Math).PIcircle.radius}
- #{disc.title + ‘by’ +disc.artist}
- #{counter.total == 100}
- #{counter.total eq 100}
计算正则表达式
通过matches对String类型的文本做运算,比如
#{admin.email matchs ‘[a-zA-Z0-9._%±]@[a-zA-Z0-9.-]+\.com’}
计算集合
- #{jukebox.songs[4].title}
- #{jukebox.songs[T(java.lang.Math.random()*jukebox.songs.size())].title}
SpEL提供了查询运算符(.?[]),他会用来对集合进行过滤,得到集合的一个子集
比如,下面会得到所有artist为 Aerosmith的所有歌曲
#{jukebox.songs.?[artist eq ‘Aerosmith’]}
另外还有两个表达式, .1 和 .$[] 他们分别用来查找集合中第一个匹配和最后一个匹配项
#{jukebox.songs.2},这个是找到第一个匹配项
最后还有一个投影功能,就类似于JavaScript的map功能, .![],返回一个新的集合
#{jukebox.songs.![title]}
以上这些运算符可以组合起来一起用。