第1部分 概述(略)
第1章 Spring框架的由来
1.1 框架概述
最主要的路径及模块:
IOC -> AOP -> MyBatis等的集成 -> 事务管理 -> SpringMVC.
关系是IOC是AOP的基础,AOP是事务的基础。
第2部分 IOC
第2章 IOC基本概念
整个依赖注入,最好的理解方式就是把“注入”两个字换成“赋值”。何时注入,即何时赋值。
2.2 IOC注入方式
关于IoC模式,最权威的解释是Martin Fowler的《Inversion of Control Containers and the Dependency Injection pattern》一文。其中提到了三种注入方式,也就是三种赋值方式:
- 构造方法注入(constructor injection)。比较常用。
- setter方法注入(setter injection)。比较常用。
- 接口注入(interface injection)。这种方式比较麻烦,对代码有侵入性,因此现在很少使用了。
下面我们一一解释这三种注入方式:
以如下这个Bean来举例:
public class FXNewsProvider {
private IFXNewsListener newsListener;
private IFXNewsPersister newPersistener;
public void getAndPersistNews() {
String[] newsIds = newsListener.getAvailableNewsIds();
if(ArrayUtils.isEmpty(newsIds)) {
return;
}
for(String newsId : newsIds) {
FXNewsBean newsBean = newsListener.getNewsByPK(newsId);
newPersistener.persistNews(newsBean);
newsListener.postProcessIfNecessary(newsId);
}
}
}
2.2.1 构造方法注入
即通过构造方法设置属性值。容器检查对象的构造方法,根据构造方法要求的入参注入就行了。由于同一个对象不能构造两次,因此对象的构造和生命周期要交给容器来管理。
// 构造方法
public FXNewsProvider(IFXNewsListener newsListner,IFXNewsPersister newsPersister) {
this.newsListener = newsListner;
this.newPersistener = newsPersister;
}
2.2.2 setter方法注入
这个也很好理解,直接通过setter方法设置属性值。
public class FXNewsProvider {
private IFXNewsListener newsListener;
private IFXNewsPersister newPersistener;
public IFXNewsListener getNewsListener() {
return newsListener;
}
public void setNewsListener(IFXNewsListener newsListener) {
this.newsListener = newsListener;
}
public IFXNewsPersister getNewPersistener() {
return newPersistener;
}
public void setNewPersistener(IFXNewsPersister newPersistener) {
this.newPersistener = newPersistener;
}
}
2.2.3 接口注入
即通过接口的方法设置属性值。其实就是定义一个接口,然后这个接口里面专门规定了一个注入方法,也就是给属性赋值的方法,这个方法的参数列表当然就是需要赋值的属性啦。也就是用接口方法的形式,给容器提供了注入的参考。需要注入的时候,被注入的对象实现这个接口及注入方法就行了。
相当于用接口把所有的注入方法抽出来了,然后被注入的对象再实现一遍该接口。
2.3 IOC的解决了什么问题举例
封装了依赖管理。例如我们要new一个对象时,这个对象还有其他属性值,那么我们需要new一堆对象出来,而且必须从最基层的对象开始new。想一想有多痛苦吧,想吃个番茄炒蛋,我们得先从种番茄、养鸡开始做起。所以IOC封装了这些复杂的级联依赖,把他们统一管理起来了。
属性可以灵活运用多态,从而提供灵活的扩展性。
如下,如果Fruit写死为
Banana banana = new Banana;
,那么我每天的早餐就只能吃香蕉了。现在改用IoC注入,并声明为多态的Fruit,那么我早餐想吃啥水果就吃啥水果。public class EatBreakFast{ private Fruit fruit; }
便于单元测试。
上述的类中,我们可以提供MockFruit的单元测试类,它实现了Fruit接口。然后我们可以自己实现Fruit的方法,加入各种测试需要的行为,从而改变fruit的行为,便于提供各种测试需求。
总之一句话,IoC是用来解耦的:
- 封装复杂级联依赖:IoC把复杂的级联依赖封装起来统一管理。
- 便于实现类扩展:更换实现类时,还可以结合多态,提供了属性的灵活性。
- 避免了对象重复创建:避免了大量不必要的对象重复创建。
提问
IOC有几种注入方式?分别举例说明?构造方法、setter方法、接口方法。其实就是在哪里赋值而已。
IOC解决了什么问题?为什么要有IOC?封装复杂依赖,实现多态,便于单元测试。1.创建了大量重复对象,造成大量资源浪费。2.更换实现类时,要改动很多地方。3.创建和配置组件的工作是很繁杂的,组件调用方用起来很麻烦。这一切问题,就是因为Bean的构建和使用没有解耦。所以IOC用来解耦Bean的创建/生命周期管理和使用。扩展:AOP则解决了切面逻辑编写繁琐,有多少个业务方法就要写多少次的问题。是面向对象的补充。
IOC容器需要解决什么问题?创建对象,管理依赖。
第3章 IoC Service Provider
这里的 IoC Service Provider是一个抽象的概念,它是IoC服务的提供者,也就是实现IoC的东东。可以是一段代码,也可以是一个IoC框架或容器实现,比如Spring的IoC容器。
3.1 IoC Service Provider是干啥的
也就是 IoC Service Provider要解决的是什么问题。主要是两个:
- 业务对象的构建管理。也就是对象的生成嘛,很好理解。包括业务对象和依赖对象。
- 业务对象间的依赖关系。IoC通过之前生成的对象,以及分析他们之间的依赖关系,将各个依赖的对象注入。
3.2 IoC Service Provider怎么管理依赖关系
也就是如何管理对象间的依赖信息。主要有以下三种:
3.2.1 直接编码方式
直接编码。就是很暴力的直接编码实现:
IContainer container = ...;
container.register(FXNewsProvider.class, new FXNewsProvider());
container.register(IFXNewsListener.class, new DowJonesNewsListener());
...
FXNewsProvider newsProvider = (FXNewsProvider)container.get(FXNewsProvider.class);
newProvider.getAndPersistNews();
3.2.2 xml配置方式
配置文件方式,我们用的是xml,这种方式很熟悉了:
<bean id="newsProvider" class="..FXNewsProvider">
<property name="newsListener">
<ref bean="djNewsListener"/>
</property>
<property name="newPersistener">
<ref bean="djNewsPersister"/>
</property>
</bean>
<bean id="djNewsListener" class="..impl.DowJonesNewsListener"> </bean> <bean id="djNewsPersister" class="..impl.DowJonesNewsPersister"> </bean>
3.2.3 注解方式
也叫元数据方式。现在很常用,特别是SpringBoot出现后,注解成为一种最佳实践。
@Service
public class Eat {
@Autowired
private Food food;
}
提问
IOC容器管理依赖关系有哪些手段?或者说有哪些描述手段?直接编码、xml配置、注解。
Spring提供的IOC容器有哪些?Bean Factory和ApplicationContext
第4章 Spring的IoC容器之Bean Factory
Spring提供两种容器类型:
- BeanFactory。提供基础的IoC容器服务。默认采用lazy-load,所以启动也快。对象用到的时候才会生成及绑定。
- ApplicationContext。间接继承自BeanFactory,并扩展了其它接口功能。提供了事件发布等高级特性。对象在容器启动后就全部加载了。
以下是两者关系,可以看到ApplicationContext
还继承了其他三个接口:
然后以下是BeanFactory接口的定义:
public interface BeanFactory (
String FACTORY_BEAN_PREFIX="&";
Object getBean(string name)throws BeansException;
Object getBean(String name, Class requiredType)throws BeansException;
Object getBean(String name, Object[] args) throws BeansException;
boolean containsBean(String name);
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name,Class targetType) throws NoSuchBeanDefinitionException;
Class getType(String name) throws NoSuchBeanDefinitionException;
String[] getAliases(String name);
}
可以看到基本是查询的方法。然后一般只有主入口类才会和这个容器API直接耦合。
4.1 BeanFactory怎么用
在没有BeanFactory
之前,我们要用到对象是直接new。
FXNewsProvider newsProvider = new FXNewsProvider();
newsProvider.getAndPersistNews();
有了BeanFactory
(或ApplilcationContext
)之后,我们的开发流程需要三步。配置xml实现依赖管理,初始化BeanFactory
,然后直接从BeanFactory
拿bean即可。
- 配置xml
<beans><!-- 构造方法注入 -->
<bean id="djNewsProvider" class="..FXNewsProvider">
<constructor-arg index="0">
<ref bean="djNewsListener"/>
</constructor-arg>
<constructor-arg index="1">
<ref bean="djNewsPersister"/>
</constructor-arg>
</bean>
</beans>
- 根据配置初始化
BeanFactory
或ApplicationContext
,然后直接拿bean
即可。
示例1:new XmlBeanFactory(new ClassPathResource(String xmlPath)));
BeanFactory container = new XmlBeanFactory(new ClassPathResource("配置文件路径"));
FXNewsProvider newsProvider =(FXNewsProvider)container.getBean("djNewsProvider"); newsProvider.getAndPersistNews();
示例2:new ClassPathXmlApplicationContext(String xmlPath);
ApplicationContext contaimer = new ClassPathXmlApplicationContext("配置文件路径");
FXNewsProvider newsProvider =(FXNewsProvider)container.getBean("djNewsProvider"); newsProvider.getAndPersistNews();
示例3:new FileSystemXmlApplicationContext(String xmlPath)
;
ApplicationContext contaimer = new FileSystemXmlApplicationContext("配置文件路径");
FXNewsProvider newsProvider =(FXNewsProvider)container.getBean("djNewsProvider"); newsProvider.getAndPersistNews();
4.2 BeanFactory如何管理对象注册及依赖信息
业务对象间的依赖绑定关系肯定需要记下来,像前面提到的一样,BeanFactory
可以用直接编码、配置文件(xml或properties)、注解三种方式来记录管理。
1.直接编码
严格来讲这种方式不适合单独提出来作为一种记录方式。不过我们可以用它来了解BeanFactory
底层是怎么玩的。
// 通过编码方式使用BeanFactory实现FX新闻相关类的注册及绑定
public static void main(String[] args) {
// 创建容器
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
BeanFactory container = (BeanFactory) bindviaCode(beanRegistry);
// 获取Bean
FXNewsProvider newsProvider = (FXNewsProvider) container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
}
// 对象绑定至容器
public static BeanFactory bindviaCode(BeanDefinitionRegistry registry) {
// 1.获取bean定义Bean
AbstractBeanDefinition newsProvider = new RootBeanDefinition(FXNewsProvider.class, true);
AbstractBeanDefinition newsListener = new RootBeanDefinition(DowJonesNewsListener.class, true);
AbstractBeanDefinition newsPersister = new RootBeanDefinition(DowJonesNewsPersister.class, true);
// 2.将bean定义Bean注册到容器中
registry.registerBeanDefinition("djNewsProvider", newsProvider);
registry.registerBeanDefinition("djListener", newsListener);
registry.registerBeanDefinition("djPersister", newsPersister);
// 3.设置bean定义Bean的依赖关系
// a.可以通过构造方法注入方式
ConstructorArgumentValues argValues = new ConstructorArgumentValues();
argvalues.addIndexedArgumentValue(0, newsListener);
argValues.addIndexedArgumentValue(1, newsPersister);
newsProvider.setConstructorArgumentValues(argValues);
// b.或者通过settr方法注入方式
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.addPropertyValue(new ropertyValue("newsListener", newsListener));
propertyValues.addPropertyValue(new PropertyValue("newPersistener", newsPersister));
newsProvider.setPropertyValues(propertyValues);
// 4.绑定完成
return (BeanFactory) registry;
}
几个重要的接口:
- BeanFactory接口:Bean容器,定义了容器内Bean的访问方式。
- BeanDefinition接口:“Bean定义”的Bean,封装了各个文件中的Bean定义。
- BeanDefinitionRegistry接口:定义了Bean的注册逻辑。
上述代码中,DefaultListableBeanFactory
同时实现了BeanFactory
接口和BeanDefinitionRegistry
接口。各个类关系如下图:
关于BeanDefinition:
- 每个受管的对象,在容器中都会有一个BeanDefinition的实例(instance)与之对应,该实例负责保存对象的所有必要信息,包括其对应的对象的class类型、是否是抽象类、构造方法参数以及其他属性等。
- 当客户端向BeanFactory请求对象时,BeanFactory会根据BeanDefinition的信息为客户端返回一个完备可用的对象实例。
- 注意到BeanDefinition的Bean也是Bean,所以bean定义的Bean要先注册到容器。
- BeanDefinition的两个主要实现类:
RootBeanDefinition
和ChildBeanDefinition
。
2.外部配置文件方式
Spring的IOC容器主要支持两种格式:Properties和XML。也可自己引入其他格式。
BeanDefinitionReader接口:用于将配置文件内容映射到BeanDefinition
。
采用外部文件配置时,一般处理顺序如下:
- BeanDefinitionReader把文件映射到BeanDefinition:由文件格式对应的BeanDefinitionReader实现类相应的配置文件内容映射到BeanDefinition。
- BeanDefinition注册到BeanDefinitionRegistry:BeanDefinitionReader将映射后的BeanDefinition注册到一个BeanDefinitionRegistry。
- BeanDefinitionRegistry完成Bean的注册和加载。
BeanDefinitionReader相当于桥梁:大部分工作,包括解析文件格式、装配BeanDefinition等,都是由BeanDefinitionReader的相应实现类来做的,BeanDefinitionRegistry只负责保管而已。BeanDefinitionReader相当于一个桥梁,一头连接文件路径读入文本,然后装配为BeanDefinition,最后另外一头注册到BeanDefinitionRegistry。
整体流程伪代码:
BeanDefinitionRegistry beanRegistry = <某个BeanDefinitionRegistry实现类,通常为DefaultListableBeanFactory>;
BeanDefinitionReader beanDefinitionReader = new BeanDefinitionReaderImpl(beanRegistry);
beanDefinitionReader.loadBeanDefinitions("配置文件路径");
// 现在我们就取得了一个可用的BeanDefinitionRegistry实例
1.Properties配置格式的加载
Spring提供了org.springframework.beans.factory.support.PropertiesBeanDefinitionReader
类用于加载Properties格式配置文件,所以我们不用自己去实现BeanDefinitionReader。
假设配置文件内容如下:
# djNewsProvider是beanName,后同
djNewsProvider.(class)=..FXNewsProvider
#---------通过构造方法注入的时候-
djNewsProvider.S0(ref)=djListener
djNewsProvider.$1(ref)=djPersisten
# --------通过setter方法注入的时候------
# djNewsProvider.newsListener(ref)=djListener
# djNewsProvider.newPersistener(ref)=djPersister
djListener.(class)=..impl.DowJonesNewsListener
djPersister.(class)=..impl1.DowJonesNewsPersiste
上述文件很清晰明了,不再解析含义。
以下是文件加载过程,和我们前文描述的一样:
// 加载Properties配置的BeanFactory的使用演示
public static void main(String[] args) {
DefaultListableBeanFactory beanDefinitionRegistry = new DefaultListableBeanFactory();
BeanFactory container = (BeanFactory) bindViaPropertiesFile(beanDefinitionRegistry);
FXNewsProvider newsProvider = (FXNewsProvider) container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
}
public static BeanFactory bindViaPropertiesFile(BeanDefinitionRegistry beanDefinitionRegistry) {
PropertiesBeanDefinitionReader reader =
new PropertiesBeanDefinitionReader(beanDefinitionRegistry);
reader.loadBeanDefinitions("classpath:../../binding-config.properties");
return (BeanFactory) beanDefinitionRegistry;
}
这样,对象的注册和依赖绑定的代码就封装到了BeanDefinitionReader里,这里就是PropertiesBeanDefinitionReader。
2.XML配置格式的加载
Spring提供了XmlBeanDefinitionReader
类来加载XML格式配置文件
Spring2.x后,XML文件在DTD(Document Type Definition)文档格式约束的基础上,增加了基于XSD(XML Schema Definition)的约束方式。
假设FX新闻系统对象配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" ➥
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="djNewsProvider" class="..FXNewsProvider">
<constructor-arg index="0">
<ref bean="djNewsListener"/>
</constructor-arg>
<constructor-arg index="1">
<ref bean="djNewsPersister"/>
</constructor-arg>
</bean>
<bean id="djNewsListener" class="..impl.DowJonesNewsListener">
</bean>
<bean id="djNewsPersister" class="..impl.DowJonesNewsPersister">
</bean>
</beans>
上述文件很清晰,不再解释。
然后我们把XML的内容加载到BeanFactory:
public static void main(String[] args) {
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
BeanFactory container = (BeanFactory) bindViaXMLFile(beanRegistry);
FXNewsProvider newsProvider =
(FXNewsProvider) container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
}
public static BeanFactory bindViaXMLFile(BeanDefinitionRegistry registry) {
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry);
reader.loadBeanDefinitions("classpath:../news-config.xml");
return (BeanFactory) registry;
// 或者整个代码直接替换为
//return new XmlBeanFactory(new ClassPathResource("../news-config.xml"));
}
同样的逻辑,XmlBeanDefinitionReader把XML文件解析,映射到BeanDefiniton,然后加载到BeanDefinitionRegistry(这里是DefaultListableBeanFactory)。
Spring还在DefaultListableBeanFactory的基础上构建了XmlBeanFactory实现类,用于简化XML格式配置加载。加上述代码最后一行。
3.注解方式
注解方式后续会详细讲。这里简要提下。
假设用用@Autowired和@Component声明要注入的Bean如下:
@Component
public class FXNewsProvider {
@Autowired
private IFXNewsListener newsListener;
@Autowired
private IFXNewsPersister newPersistener;
public FXNewsProvider(IFXNewsListener newsListner, IFXNewsPersister newsPersister) {
this.newsListener = newsListner;
this.newPersistener = newsPersister;
}
// ...
}
@Component
public class DowJonesNewsListener implements IFXNewsListener {
// ...
}
@Component
public class DowJonesNewsPersister implements IFXNewsPersister {
// ...
}
只需在Spring的配置文件中增加一个“触发器”即可完成Bean注入:
<?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:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<!-- 关键的这一行,其实跟springboot声明base包是一样的 -->
<context:component-scan base-package="cn.spring21.project.base.package"/>
</beans>
注意到<context:component-scan>,它会到指定的包(package)下面扫描标注@Component的类,如果找到,则将它们的Bean添加到容器进行管理,并根据它们所标注的@Autowired为这些类注入符合条件的依赖对象。
配置好扫描器后,在代码中启动容器后,直接使用Bean即可:
public static void main(String[] args) {
// 启动容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("配置文件路径");
// 直接获取Bean即可
FXNewsProvider newsProvider = (FXNewsProvider) container.getBean("FXlewsProvider")
newsProvider.getAndPersistNews();
}
4.3 XML文件有哪些元素
本节详细解析XML里有哪些元素,每个元素用来干嘛。
首先是DTD和XSD的约束声明,略。
所有注册到容器的业务对象,在Spring中都叫做Bean。
1.<beans>元素
一、beans包含的元素
可包含的元素有*<description>、<import>、<alias>、<bean>*,都是0-n个,如下:
二、beans的属性
属性有,这些属性好多在bean元素里也有,这里相当于是对所有bean批量设置:
- default-lazy-init。取值:true或false。默认值:false。用来标志是否对所有的<bean>进行延迟初始化。
- default-autowire。取值:no、byName、byType、constructor、autodetect。默认值:no。如果使用自动绑定的话,用来标志全体bean使用哪一种默认绑定方式。
- default-dependency-check。取值:none、objects、simple、all。默认值:no。标记依赖检查范围。
- default-init-method。所有管辖的<bean>的统一初始化方法名。(若有统一初始化方法的话)
- default-destroy-method。与default-init-method相对应,所有管辖的bean的对象销毁方法名。(若有)
三、<description>、<import>和<alias>
一般这三个元素不是必须的。
- <description>:在配置的文件中加入一些描述性信息,声明备注之类的。
- <import>:引入其他XML文件到本文件,主要用来模块划分,在主文件中引入子文件。比如,如果A.xml中的<bean>定义可能依赖B.xml中的某些<bean>定义,那么就可以在A.xml中使用<import>将B.xml引入到A.xml,以类似于*<import resource=”B.xml”/>*的形式。一般来说用处不大,因为容器实际上可以同时加载多个配置。
- <alias>:用来给某些<bean>起一些“外号”(别名)。例如*<alias name=”dataSourceForMasterDatabase”alias=”masterDatasource”/>*,前面是本名,后面是别名。
2.<bean>元素
示例如下:
<bean id="djNewsListener"
name="/news/d)NewsListener,dowJonesNewsListener"
class="..impl.DowJonesNewsListener">
</bean>
属性:
- id:bean的唯一标志符。即bean的beanname。某些情况下,无需根据beanName明确依赖关系时,id可省略。
- name:id的别名。可以使用更灵活的字符,如”/“和”,”。
- class:指定对象的类型。大部分情况下,该属性是必须的。仅在少数情况下无需指定,如后面将提到的在使用抽象配置模板的情况下。
3.如何表达bean之间的依赖
在Spring的IoC容器的XML配置中,如何表达对象间的依赖?
其实依赖注入除了直接编码,就两种方式:构造方法注入、setter方法注入。我们只需要看下这两种方法,在XML中如何表达即可。
1.constructor-arg元素:构造方法注入Bean的构造方法参数
使用<constructor-arg>表示构造方法参数,多个入参时用多个该元素表示。简写形式可以直接把元素写到属性里。如下:
<bean id="djNewsProvider" class="..FXNewsProvider">
<constructor-arg>
<ref bean="djNewsListener"/>
</constructor-arg>
<constructor-arg>
<ref bean="djNewsPersister"/>
</constructor-arg>
</bean>
<!-- 简写形式,元素直接写到属性里 -->
<bean id="djNewsProvider" class="..FXNewsProvider">
<constructor-arg ref="djNewsListener"/>
<constructor-arg ref="djNewsPersister"/>
</bean>
当对象有多个构造方法,或者构造方法有多个入参时,用type属性和index属性指定入参值的类型或映射到第几个入参:
- type属性:指定入参值的类型。用于区分多个构造方法,但入参数量一致时的情况。没有指定时默认取遇到的第一个方法。
- index属性:指定入参值映射到构造方法的第几个入参。用于构造方法有多个入参时,进行区分映射。没有指定时默认按参数值的声明顺序,去按顺序匹配入参。
<bean id="mockBO" class="..MockBusinessObject">
<!-- type类型:比如有两个构造方法名字一样,但是一个入参是int,一个入参是String。以下指明入参类型为int -->
<constructor-arg type="int">
<value>111111</value> <!-- value 做为元素时按顺序赋值。也可以写成属性,用index指明位置即可 -->
</constructor-arg>
</bean>
<!-- value也可以写成属性,用index指明位置即可 -->
<bean id="mockBO" class="..MockBusinessObject">
<constructor-arg index="1" value="11111"/>
<constructor-arg index="0" value="22222"/>
</bean>
2.property元素:setter方法注入Bean的对象属性字段值
构造方法提供了<constructor-arg>,setter方法则提供了*<property>*。
使用<property>表示实例的属性字段,,元素和属性也可以互换:
- name属性:指定该<property>注入的对象所对应的实例变量名称。
- value元素:具体的值
- ref元素:具体的依赖对象引用
<bean id="djNewsProvider" class=".FXNewsProvider">
<property name="newsListener">
<ref bean="djNewsListener"/>
</property> <property name="newPersistener">
<ref bean="djNewsPersister"/>
</property>
</bean>
<!-- 元素也可以简化为属性 -->
<bean id="djNewsProvider" class="..FXNewsProvider">
<property name="newsListener" ref="djNewsListener"/>
<property name="newPersistener" ref="djNewsPersister"/>
</bean>
<!-- 如果只用<property>进行依赖注入,要确保对象已提供了默认的构造方法。-->
如果只用<property>进行依赖注入,要确保对象已提供了默认的构造方法。
如果有需要,两种注入方式可以同时使用。即同时提供Bean的构造方法参数和Bean的属性字段。
<bean id="mockBO" class="..MockBusinessObject">
<constructor-arg value="11111"/>
<property name="dependency2" value="22222"/>
</bean>
对应的被注入的Bean如下,构造方法和setter方法提供了两种注入方式的支持:
public class MockBusinessObject {
private String dependency1;
private String dependency2;
public MockBusinessObject(String dependency) 12 {
this.dependency1 = dependency;
}
public void setDependency2(String dependency2) {
this.dependency2 = dependency2;
}
//...
}
3.property和constructor-arg元素中可用的属性或元素
前文提到了两种注入方法。本小节来列举它们可以使用的注入值的类型,包括以下,它们对<property>和<constructor-arg>都是通用的:
- bean
- ref
- idref
- value:简单的数据类型,包括String。容器在注入的时候,会做适当的转换(稍后会介绍)。
- null
- list
- set
- map
- props
(1) <value>。简单的数据类型,包括String。容器在注入的时候,会做适当的转换(稍后会介绍)。是最“底层”的元素,它内部不能再嵌套使用其他元素。
<constructor-arg>
<value>111111</value>
</constructor-arg>
<property name="attributeName">
<value>22222</value>
</property>
<!-- 也可简化为属性 -->
<constructor-arg value="11111"/>
<property name="attributeName" value="2222"/>
2)<ref>。用ref来引用容器中其他的对象实例,可以通过ref的local、parent和bean属性来指定引用的对象的beanName。是最“底层”的元素,它内部不能再嵌套使用其他元素。
<!-- <ref>及其local、parent和bean属性的使用 -->
<bean></bean><constructor-arg><ref local="djNewsPersister"/></constructor-arg></bean>
或者
<bean><constructor-arg><ref parent="djNewsPersister"/></constructor-arg></bean>
或者
<bean><constructor-arg><ref bean="djNewsPersister"/></constructor-arg></bean>
local、parent和bean的区别:
- local:只指定当前配置文件中定义的对象(可获得XML解析器的id约束验证支持)。
- parent:只指定在当前容器的父容器中定义的对象引用。
- bean:通吃,通常直接用bean来指定对象引用即可。
(3) <idref>。当要注入的是所依赖的对象的名称,而不是它的引用时使用。一般可直接用value注入,但使用idref更规范,因为使用idref,容器在解析配置的时候就可以帮助检查这个beanName到底是否存在。
<bean>
<property name="newsListenerBeanName">
<idref bean="djNewsListener"/>
</property>
</bean>
(4) 内部*<bean>*。如果bean只有一个,我们不想用ref引用的话,可以直接在bean里再嵌套bean元素。这样的话,被引用的bean只能被当前bean引用到。
<bean id="djNewsProvider" class="..FXNewsProvider">
<constructor-arg index="0">
<bean class="..impl.DowJonesNewsListener"></bean> <!-- 直接引用内部bean -->
</constructor-arg>
<constructor-arg index="1">
<ref bean="djNewsPersister"/> <!-- 通过ref引用bean -->
</constructor-arg>
</bean>
(5)*<list>*。对应java.utl.List
及其子类或者数组类型的依赖对象。实际较少用。
public class MockDemoObject {
private List param1;
private String[] param2;
// ...
// 相应的setter和getter方法
// ...
}
配置为
<property name="paraml">
<list>
<value>something</value>
<ref bean="someBeanName"/>
<bean class="..."/>
</list>
</property>
<property name="param2">
<list>
<value>stringValuel</value>
<value>stringValue2</value>
</list>
</property>
(6)<set>。<list>用于有序地注入一系列依赖,*<set>则是无序*的。实际较少用。
Java:
public class MockDemoObject {
private Set valueSet;
//必要的setter和getter方法 ..·
}
XML:
<property name="valueSet">
<set>
<value> something</value>
<ref bean="someBeanName"/>
<bean class="..."/>
<list>
...
</list>
</set>
</property>
(7)*<map>*。与list和set类型。map用于键值对。
Java:
public class MockDemoObject {
private Map mapping;
// 必要的setter和getter方法
// ...
}
XML:
<property name="mapping">
<map>
<entry key="strValueKey">
<value>something</value>
</entry>
<entry>
<key>objectKey</key>
<ref bean="someObject"/>
</entry>
<entry key-ref="lstKey">
<list></1ist>
</entry>
<!-- 也可以用属性代替元素 -->
<entry key="strValueKey" value="something"/>
<entry key-ref="" value-ref="someObject"/>
</map>
</property>
可以内嵌任意多个<entry>,每个<entry>需要指定键和值:
- 指定entry的键。可以使用<entry>的属性——key或者key-ref来指定,也可使用<entry>的内嵌元素<key>指定。
- 指定entry的值。除了<key>是用来指定键的,其他元素可以任意使用,包括前面提到的,以及后面要提到的<props>。
(8)*<props>*。简化后的<map>,对应配置类型为java.util.Properties
的对象依赖。只能指定String类型的键和值。每个<props>可以嵌套多个<prop>。
java:
public class MockDemoObject {
private Properties emailAddrs;
// 必要的setter和getter方法
...
}
xml:
<property name="valueSet">
<props>
<prop key="author">fujohnwang@gmail.com</prop>
<prop key="support">support@spring21.cn</prop>
...
</props>
</property>
(9)*<null/>*。对于string类型来说,如果通过value以这样的方式指定注入,即<value></value>,那么,得到的结果是””,而不是null。所以,要表示null,请使用<null/>。
java:
public class MockDemoObject {
private String param1;
private Object param2;
// 必要的setter和getter方法
// ...
}
xml:
<property name="paraml">
<null/>
</property>
<property name="param2">
<null/>
</property>
4. depends-on属性:指定非显式依赖
前面提到的所有其他元素用于标记显示依赖,depends-on用于标记非显示依赖。
可以通过前面提到的所有元素,来显示的指定bean之间的依赖关系。这样在Spring在初始化当前bean之前,就可以先初始化它依赖的其他bean。
但是,如果某些时候,我们没有通过类似<ref>的元素明确指定对象A依赖于对象B的话,如何让容器在实例化对象A之前首先实例化对象B呢?答案是用depends-on标记。
Java:
public class SystemConfigurationSetup {
static {
DOMConfigurator.configure("配置文件路径");
// 其他初始化代码
}
// ...
}
如上,如果ClassA需要使用log4j,就需要在bean定义中使用depends-on来标记非显示依赖,来要求容器在初始化自身实例之前*先实例化SystemConfigurationSetup
*。
如果有多个依赖,可以用“,”隔开:
<bean id="classAInstance"class="..ClassA" dapends-on="cofigsetup,configsetup2,.."/>
<bean id="configSetup" class="SystemConfigurationSetup"/>
<bean id="configSetup2" class="SystemConfigurationSetup2"/>
5.autowire属性:指定依赖绑定
通过<bean>的autowire属性直接指定自动绑定模式,就无需通过手工去明确指定该bean定义相关的依赖关系。
示例:
<bean id="beanName" class="..." autowire="no"/>
autowire有5种自动绑定模式:
- no
- byName
- byType
- constructor
- autodetect
(1) no
容器默认的自动绑定模式。即不采用任何形式的自动绑定,完全依赖手工明确配置各个bean之间的依赖关系。
(2) byName
根据字段的名字,去容器中查找id与该字段名一致的bean,注入到该字段。与XML配置文件中声明的bean定义的beanName的值进行匹配。
Java:
public class Foo {
private Bar emphasisAttribute;
// ...
// 相应的setter方法定义
}
public class Bar {
// ...
}
XML配置:
<!-- 注意到只规定了byName,并没有明确的指定任何依赖关系 -->
<bean id="fooBean" class="...Foo" autowire="byName"></bean>
<!-- 注意到id的名字,和要注入的Bean中的字段名称一致 -->
<bean id="emphasisAttribute" class="...Bar"></bean>
注意到两点:
- 没有指定任何依赖关系,只是指定了byName,然后容器自己用变量的名字作为id,去容器中找bean注入。
- beanName值即字段的名字,跟注入的Bean的字段id一致。看上述的代码示例,很清晰。
(3) byType
根据字段的类型,去容器中查找id与该字段的类型一致的bean,注入到该字段。和byName类似。如果找到多个,需要手动指定“该选用哪一个”。
XML:
<bean id="fooBean" class="...Foo" autowire="byType">
</bean>
<bean id="anyName" class="...Bar">
</bean
(4) constructor
按构造方法的参数类型自动注入。
byName和byType类型的自动绑定模式是针对property的自动绑定。
constructor类型则是针对构造方法参数的类型进行的自动绑定,它同样是byType类型的绑定模式。
constructor匹配的是构造方法的参数类型。若找到多个,同样的,需要手动指定“该选用哪一个”。
Java:
public class Foo {
private Bar bar;
public Foo(Bar arg) { // 注意到这里的参数类型
this.bar = arg;
}
// ...
}
XML:
<bean id="foo" class="...Foo" autowire="constructor"/> <!-- 按构造方法的参数类型注入 -->
<bean id="bar" class="...Bar">
</bean
(5) autodetect
byType和constructor模式的结合体。
若对象拥有默认无参数的构造方法,则容器优先考虑byType的自动绑定模式。
否则,使用constructor模式。当然,若通过构造方法注入绑定后还有其他属性没有绑定,容器也会使用byType对剩余的对象属性进行自动绑定。
注意:
- 手工明确指定的绑定关系总会覆盖自动绑定模式的行为
- 自动绑定只对*”原生类型、String类型以及Classes类型以外”的对象类型有效*,否则对”原生类型、Sring类型和Clases类型”以及”这些类型的数组”应用自动绑定是无效的。
自动绑定与手动明确绑定的抉择:
- 自动绑定的优点:
- 可减少手动敲入配置信息的工作量
- 某些情况下,为当前对象增加新的依赖关系时,但只要容器中存在相应的依赖对象,就无需更改任何配置信息。
- 自动绑定的缺点:
- 自动绑定不如明确依赖关系一目了然。
- 某些情况下,自动绑定无法满足系统需要,甚至导致系统行为异常或者不可预知。根据类型(byType)匹配进行的自动绑定,如果系统中增加了另一个相同类型的bean定义,那么整个系统就会崩溃;根据名字(byName)匹配进行的自动绑定,如果把原来系统中相同名称的bean定义类型给换掉,就会造成问题,而这些可能都是在不经意间发生的。
- 使用自动绑定,我们可能无法获得某些工具的良好支持,比如Spring IDE。(现在不存在这种问题了吧~)
<beans>有一个default-autowire属性,默认为no,可用来指定所有bean的默认autowire属性。优先级低于<bean>里的autowire属性。
<beans default-autowire="byType">
<bean id="..." class="..."/>
...
</beans>
6.dependency-check属性
bean的dependency-check属性,该功能主要与自动绑定结合使用,可以帮我们检查每个对象某种类型的所有依赖是否全部已经注入完成。比较少用。
基本上有如下4种类型的依赖检查
- none。不做依赖检查。不指定时默认也是这个属性。
- simple。对简单属性类型以及相关的collection进行依赖检查,对象引用类型的依赖除外。
- object。只对对象引用类型依赖进行检查。
- all。将simple和object相结合,会对简单属性类型以及相应的collection和所有对象引用类型的依赖进行检查。
7. lazy-init属性
延迟初始化,即懒加载。
主要针对ApplicationContext容器的bean。与BeanFactory默认懒加载不同,ApplicationContext在容器启动时,就会马上对所有的“singleton的bean定义”进行实例化。
也可以通过<beans>进行全局设置:
<beans default-lazy-init="true">
<bean id="lazy-init-bean" class="..."/>
<bean id="not-lazy-init-bean" class="...">
<property name="propName">
<ref bean="lazy-init-bean"/>
</property>
</bean>
...
</beans>
4.子bean可继承父bean的property声明
横向上,bean之间有各种横向依赖关系。
纵向上,各个bean之间也就继承关系,确切来讲,是“类之间的继承关系”。
bean的parent属性:用于子类bean直接引用父类bean里面的property声明。指明了父类,那么父类里面已经声明过的字段,就不用在子类里面再声明一遍。
举例:
假如Java如下:
class SpecificFXNewsProvider extends FXNewsProvider {
private IFXNewsListener newsListener;
private IFXNewsPersister newPersistener;
...
}
正常情况下,声明如下:
<bean id="superNewsProvider" class="..FXNewsProvider">
<property name="newsListener">
<ref bean="djNewsListener"/>
</property>
<property name="newPersistener"> <!-- 注意newPersistener字段的声明 -->
<ref bean="djNewsPersister"/>
</property>
</bean>
<bean id="subNewsProvider"class="..SpecificFXNewsProvider">
<property name="newsListener">
<ref bean="specificNewsListener"/>
</property>
<property name="newPersistener"> <!-- 注意newPersistener字段的声明 -->
<ref bean="djNewsPersister"/>
</property>
</bean>
使用了parent属性,则已经在父类里面声明的newPersistener字段就不用再在子类里声明:
<bean id="superNewsProvider" class="..FXNewsProvider">
<property name="newsListener">
<ref bean="djNewsListener"/>
</property>
<property name="newPersistener"> <!-- 注意newPersistener字段的声明 -->
<ref bean="djNewsPersister"/>
</property>
</bean>
<bean id="subNewsProvider" parent="superNewsProvider"
class="..SpecificFXNewsProvider"> <!-- 子类指明了parent,所以不用再声明newPersistener字段 -->
<property name="newsListener">
<ref bean="specificNewsListener"/>
</property>
</bean>
abstract属性:声明为true时说明这个bean定义不需要实例化。所以bean不用指明class。容器在初始化时不会实例化abstract的bean。所以可以把某个bean声明为abstract,来避免容器把它实例化。在ApplicationContext里很有用,因为它默认启动时实例化所有bean。
举例,用abstract和parent来复用bean模板:
<!-- abstract,不会被实例化 -->
<bean id="newsProviderTemplate" abstract="true">
<property name="newPersistener">
<ref bean="djNewsPersister"/>
</property>
</bean>
<!-- 声明parent为模板bean,复用它的property声明 -->
<bean id="superNewsProvider" parent="newsProviderTemplate" class="..FXNewsProvider">
<property name="newsListener">
<ref bean="djNewsListener"/>
</property>
</bean>
<!-- 声明parent为模板bean,复用它的property声明 -->
<bean id="subNewsProvider" parent="newsProviderTemplate" class="..SpecificFXNewsProvider">
<property name="newsListener">
<ref bean="specificNewsListener"/>
</property>
</bean>
5.bean的scope属性
scope属性:对象的作用域,或生命周期。即对象所处的限定场景或存活时间。容器在对象进入其相应的scope之前,会生成并装配这些对象,在该对象不再处于这些scope的限定之后,通常会销毁这些对象。
Spring的scope类型有:singleton和prototype。2.0后引入了只用于Web应用的另外三种:request、session和global session类型。
bean相当于是构建对象的模板,而scope则指明需要根据这个模板构造多少对象实例,又该让这些构造完的对象实例存活多久。
声明方式:
<!-- DTD方式: -->
<bean id="mockObject1" class="...MockBusinessObject" singleton="false"/>
<!-- XSD方式: -->
<bean id="mockObject2" class=".MockBusinessObject" scope="prototype"/>
下面进行详细解释。
(1) singleton
- 构建几个:单例模式,在Spring的IoC容器中只存在一个实例,所有对该对象的引用将共享这个实例 。
- 生命周期:容器启动后,在第一次被请求时初始化之后,之后将一直存活到容器退出。
与设计模式的Singleton模式有区别:Singleton模式是在同一个Classloader中只存在一个这种类型的实例。而singleton属性的bean则是由容器来保证这种类型的bean在同一个容器中只存在一个共享实例。
不指定scope时,容器默认的scope是singleton。
(2)prototype
- 构建几个:每次都生成新的实例给请求方。
- 生命周期:交给请求方管理,容器不再存实例的引用。
一般使用prototype的实例都是一些有状态的实例,例如每个用户信息的对象。
(3)request
Spring容器
,即xmlwebApplicationContext
会为每个HTTP请求创建一个全新的对象来给当前请求使用。请求结束后,该对象实例的生命周期结束。如果有10个http请求,那么就会生成10个RequestProcessor对象。
request可以看作是prototype的一种特例,语意上差不多,只是使用场景不太一样。
(4)session
Spring容器会为每个独立的session
创建一个全新的对象实例。
与request scope的bean相比,拥有session scope的bean的实例活的更久,其他方面没什么差别。
(5)global session
global session
应用在基于portlet
的Web应用程序中才有意义,它映射到portlet的global范围的session。
global session
用在普通的基于servlet的Web应用中时,容器会把它看作是普通的session
。
参考:portlet MVC 是Spring提供的另外一套组件,和 web MVC组件主要的区别:
Servlet是与表现层无关的,所以一个完整的Web应用可以只有一个Servlet作为Controller。
Portlet是与表现层相关的,表现层的多个Portlet将对应多个Concrete Portlet。
对于Web应用,我们可以将操作请求的处理流程分为两步,处理请求和展示结果。在传统Servlet/JSP应用中,请求和展示总是一起被执行的。
但是在Portlet应用中,情况发生了改变:当doView或doEdit被调用的时候,仅展示部分被调用。这造成了在Portlet应用中处理与展示两个部分的执行频率并不相同,也就造成了Portlet应用的两阶段处理模式。
(6)自定义scope
还可以自定义scope类型。
默认的singleton和prototype是硬编码到代码中的。
而request、session和global session,包括自定义scope类型,则属于可扩展的scope,它们都实现了 org.springframework.beans.factory.config.Scope
接口。接口定义如下:
public interface Scope {
// 必须实现
Object get(String name, ObjectFactory objectFactory);
// 必须实现
Object remove(String name);
void registerDestructionCallback(String name, Runnable callback);
String getConversationId();
}
自定义scope类型的步骤:
a.实现scope接口
get()和remove()必须实现,其他随意。举例:
public class ThreadScope implements Scope {
private final ThreadLocal threadScope = new ThreadLocal() {
protected Object initialValue() {
return new HashMap();
}
};
public Object get(String name, ObjectFactory objectFactory) {
Map scope = (Map) threadScope.get();
Object object = scope.get(name);
if(object==null) {
object = objectFactory.getObject();
scope.put(name, object);
}
return object;
}
public Object remove(String name) {
Map scope = (Map) threadScope.get();
return scope.remove(name);
}
public void registerDestructionCallback(String name, Runnable callback) {
}
// ...
}
b.将scope注册到容器
方法一:
一般情况下,我们可以用ConfigurableBeanFactory
的以下方法注册自定义scope
void registerScope(String scopeName, Scope scope);
如果容器为BeanFactory类型(当然,更应该实现configurableBeanFactory),可通过如下方式来注册∶
Scope threadscope = new Threadscope();
beanFactory.registersScope("thread",threadscope)
注册好之后直接使用就行了:<bean id="beanName"class=".." scope="thread"/>
方法二:
Spring也提供了一个专门用于统一注册自定义scope的BeanFactoryPostProcessor
实现,即org.springframework.beans.factory.config.CustomScopeConfigurer
。
由于ApplicationContext
可以自动识别并加载BeanFactoryPostProcessor
,所以我们直接在配置文件中,通过这个CustomscopeConfigurer注册来Scope即可:
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread" value="com.foo.ThreadsScope"/>
</map>
</property>
</bean>
6.工厂方法与 FactoryBean
场景:对于某个Bean的变量,该变量声明的只是一个接口,如何将具体的实现对象注入该bean?由于可能会实现第三方库的接口,第三方接口后面可能随时会变,此时容器的依赖注入没法帮助我们解耦接口是实现类。
例如:
public class Foo {
private BarInterface barInstance;
public Foo() {
// 我们应该避免这样做
// instance = new BarInterfaceImpl();
}
// ...
}
解决方案是使用工厂模式来解耦接口和实现类:
public class Foo {
private BarInterface barInterface;
public Foo() {
// barInterface = BarInterfaceFactory.getInstance();
// 或者
// barInterface = new BarInterfaceFactory().getInstance();
}
...
}
1.使用静态工厂方法注入Bean
a.提供工厂类
public class StaticBarInterfaceFactory {
public static BarInterface getInstance() {
return new BarInterfaceImpl();
}
}
b.在xml文件用class
和factory-method
声明对应的工厂方法
<!-- 1.ref还是声明到工厂Bean -->
<bean id="foo" class="..Foo"> <property name="barInterface">
<ref bean="bar"/>
</property>
</bean>
<!-- 2.工厂Bean的地方声明工厂方法即可。注意下面的factory-method属性 -->
<bean id="bar" class="...StaticBarInterfaceFactory" factory-method="getInstance"/>
c.如果工厂方法需要传入参数,直接使用<constructor-arg>
传入即可
例如:
public class StaticBarInterfaceFactory {
public static BarInterface getInstance(Foobar foobar) {
return new BarInterfaceImpl(foobar);
}
}
参数传入配置:
<bean id="foo" class="...Foo">
<property name="barInterface">
<ref bean="bar"/>
</property>
</bean>
<bean id="bar" class="...StaticBarInterfaceFactory" factory-method="getInstance">
<constructor-arg> <ref bean="foobar"/>
</constructor-arg>
</bean>
<bean id="foobar" class="..FooBar"/>
针对静态工厂方法实现类的bean定义,<constructor-arg>
传入的是工厂方法的参数,而不是静态工厂方法实现类的构造方法的参数。(静态工厂方法实现类也没有提供显式的构造方法)
2.使用非静态工厂方法注入Bean
表达方式可能需要稍微变一下即可。
a.提供非静态工厂方法实现类
public class NonStaticBarInterfaceFactory {
public BarInterface getInstance() {
return new BarInterfaceImpl();
}
//...
}
b.在xml文件,先注入bean,然后用factory-bean
和factory-method
声明调用的非静态工厂方法。即用实例调用该非静态工厂方法。因为工厂方法为非静态的,我们只能通过某个NonStaticBarInterfaceFactory实例来调用,配置如下:
<bean id="foo" class="..Foo"> <property name="barInterface">
<ref bean="bar"/>
</property> </bean>
<bean id="barFactory" class="...NonStaticBarInterfaceFactory"/>
<bean id="bar" factory-bean="barFactory" factory-method="getInstance"/>
c.调用时如果需要参数,和静态工厂方法一样,用<constructor-arg>
声明即可
3.使用Spring提供的FactoryBean注入Bean
FactoryBean
是Spring提供的“制式装备”。专门用来生成bean,本质上也是一个Bean。
当对象实例化比较复杂,比起xml不如使用Java代码时,或者某些第三方库不能直接注册到Spring容器时,就可以实现org.springframework.beans.factory.FactoryBean
接口,来自己进行对象实例化。
FactoryBean接口如下:
public interface FractoryBean(
// 返回该FactoryBean“生产”的对象实例
Object getObject() throws Exception;
// 返回getObject()方法所返回的对象的类型,如果预先无法确定,则返回null
Class getObjectType();
// 表明工厂方法(getObject())所“生产”的对象是否要以singleton形式存在于容器中
boolean isSingleton();
}
举例:每次都返回第二天:
FactoryBean:
import org.joda.time.DateTime;
import org.springframework.beans.factory.FactoryBean;
public class NextDayDateFactoryBean implements FactoryBean {
public Object getObject() throws Exception {
return new DateTime().plusDays(1);
}
public Class getObjectType() {
return DateTime.class;
}
public boolean isSingleton() {
return false;
}
}
xml配置:
<bean id="nextDayDateDisplayer" class="...NextDayDateDisplayer">
<property name="dateOfNextDay">
<ref bean="nextDayDate"/>
</property>
</bean>
<!-- 容器返回的是FactoryBean所“生产”的对象类型,而非FactoryBean实现本身 -->
<bean id="nextDayDate" class="...NextDayDateFactoryBean">
</bean>
注意到虽然配置的字段对应的是FacoryBean类型,但下述nextDayDate字段声明的不是FactoryBean字段,而是直接声明了他的工厂方法的返回值。也就是说,Sprign容器悄悄帮我们处理啦:
public class NextDayDateDisplayer {
private DateTime dateOfNextDay;
// 相应的setter方法
// ...
}
可以使用&符号拿到FactoryBean对象:
assertTrue(nextDayDate instanceof DateTime);
Object factoryBean= container.getBean("snextDayDate"); assertTrue(factoryBean instanceof FactoryBean); assertTrue(factoryBean instanceof NextDayDateFactoryBean);
Object factoryValue =((FactoryBean)factoryBean).getobject(); assertTrue(factoryValue instanceof DateTime);
assertNotSame(nextDayDate, factoryValue); assertEquals((DateTime)nextDayDate).getDayOfYear(),((DateTime)factoryalue).getDayOfYear());
Spring容器内部许多地方也使用FactoryBean。下面是一些常见的FactoryBean实现:
- LocalsessionFactoryBean
- sqlMapClientFactoryBean
- ProxyFactoryBean
- TransactionProxyFactoryBean
7.bean元素与方法相关的子元素
预设概念:主体对象即主对象。依赖对象即主体对象的属性字段,该字段是一个对象。
1.方法注入:<lookup-method>子元素
a.问题场景
场景:bean元素scope属性的“漏洞”:依赖对象(即属性)的scope为prototype,主体对象为singleton。Bean注入完成后,通过get方法获取依赖对象,但由于bean注入已经完成,依赖对象已绑定,所以每次返回的都是同一个对象,和依赖对象的prototype语义“不一致”。即,主体对象是单例,它的属性,即依赖对象是多例,此时每次返回都是单例。怎么处理。
代码示例:
public class MockNewsPersister implements IFXNewsPersister {
private FXNewsBean newsBean;
public void persistNews(FXNewsBean bean) {
persistNewes();
}
public void persistNews() {
System.out.println("persist bean:"+getNewsBean());
}
public FXNewsBean getNewsBean() {
return newsBean;
}
public void setNewsBean(FXNewsBean newsBean) {
this.newsBean= newsBean;
}
}
xml注入配置:
<!-- 注意singleton="false" -->
<bean id="newsBean" class="..domain.FXNewsBean" singleton="false">
</bean>
<bean id="mockPersister" class="..impl.MockNewsPersister">
<property name="newsBean">
<ref bean="newsBean"/>
</property>
</bean>
多次调用后结果:
BeanFactory container = new XmlBeanFactory(new ClassPathResource(".."));
MockNewsPersister persister = (MockNewsPersister)container.getBean("mockPersister");
persister.persistNews();
persister.persistNews();
输出:
persist bean:..domain.FXNewsBean@1662dc8 // 注意到两次调用对象是一样的
persist bean:..domain.FXNewsBean@1662dc8 // 注意到两次调用对象是一样的
b.使用方法注入方式获取对象
Spring提供的处理上述问题的手段。通过方法来注入对象。
此时Spring会用Cglib帮我们动态生成子类实现,用于替代原对象。
步骤:
- 1.让方法声明符合规定的格式。<public|protected> [abstract] <return-type> theMethodName(no-arguments);即要让方法可被子类实现或继承,方便cglib实现。
- 2.在xml里进行注入配置:a.配置依赖对象的bean。b.在主体对象的bean中,配置<lookup-method>子元素
代码示例:
就用上述的代码举例:
首先,方法的声明已符合声明规定。
进行xml注入配置,如下:
<!-- 注入依赖bean,注意到singleton="false",非单例 --> <bean id="newsBean" class="..domain.FXNewsBean" singleton="false"> </bean> <!-- 配置主体bean的 lookup-method 子元素 --> <bean id="mockPersister" class="..impl.MockNewsPersister"> <lookup-method name="getNewsBean" bean="newsBean"/> </bean>
此时再多次执行方法,发现每次返回的都是不同的对象了。(当然也可以直接在方法里直接new对象返回,这里用方法注入是为了说明方法注入的用法)。
c.使用BeanFactoryAware
接口获取对象
上述用方法注入获取对象。然后我们知道,其实不用方法注入,每次直接在方法里调用BeanFactory
的getBean("newsBean")
方法,也能每次都获取到新实例。
所以,我们只需要主体对象有BeanFactory的引用就行了。
Spring提供了BeanFactoryAware
接口,Spring会自动把容器本身注入到实现该接口的bean中。
接口代码如下:
public interface BeanFactoryAware {
void setBeanFactory(BeanFactory beanFactory) throws BeansException;
}
使用示例:
// 实现了BeanFactoryAware接口
public class MockNewsPersister implements IFXNewsPersister, BeanFactoryAware {
private BeanFactory beanFactory;
// 接口的实现方法。注意该属性。Spring会用set方法注入容器本身。
public void setBeanFactory(BeanFactory bf) throws BeansException {
this.beanFactory = bf;
}
public void persistNews(FXNewsBean bean) {
persistNews();
}
public void persistNews() {
System.out.println("persist bean:"+getNewsBean());
}
// 获取依赖对象时,直接从容器获取即可。
public FXNewsBean getNewsBean() {
return beanFactory.getBean("newsBean");
}
}
bean注入配置:
<bean id="newsBean" class="..domain.FXNewsBean" singleton="false">
</bean>
<bean id="mockPersister" class="..impl.MockNewsPersister">
</bean>
d.使用ObjectFactoryCreatingFactoryBean
获取对象
ObjectFactoryCreatingFactoryBean
是Spring提供的一个FactoryBean实现,返回一个ObjectFactory
实例。
ObjectFactoryCreatingFactoryBean
本质上也是实现了BeanFactoryAware接口。用它返回的ObjectFactory
来和容器交互。使用它的好处是*避免了业务代码直接引用BeanFactory
*。
使用步骤:
在主体对象里持有ObjectFactory字段。
public class MockNewsPersister implements IFXNewsPersister { private ObjectFactory newsBeanFactory; // 这里持有了ObjectFactory引用 public void persistNews(FXNewsBean bean) { persistNews(); } public void persistNews() { System.out.println("persist bean:"+getNewsBean()); } public FXNewsBean getNewsBean() { return newsBeanFactory.getObject(); } public void setNewsBeanFactory(ObjectFactory newsBeanFactory) { this.newsBeanFactory = newsBeanFactory; } }
在xml里配置ObjectFactory这个bean所支持注入的bean。
<bean id="newsBean" class=".domain.FXiewsBean" singleton="false"> </bean> <bean id="newsBeanFactory"class="org.springframework,beans.factory.config.ObjectFactoryCreatingFactoryBean"> <property name="targetBeanName"> <idref bean="newsBean"/> </property> </bean> <bean id="mockPersister" class="..impl.MockNewsPersister">
也可以使用ServiceLocatorFactoryBean来代替ObjectFactoryCreatingFactoryBean,该FactoryBean可以让我们自定义工厂接口,而不用非要使用Spring的ObjectFactory。
2.方法替换:<replaced-method >子元素
方法注入是通过方法来注入主体对象的依赖对象。
方法替换则类似于AOP,直接把方法逻辑都给换掉了。可以实现简单的方法拦截功能。
使用步骤:
- 实现
org.springframework.beans.factory.support.MethodReplacer
。 - 注入实现的bean。并在主体bean的<replaced-method>子元素里,声明要替换的方法名,以及刚才声明的bean即可。
使用示例,替换getAndPersistNews方法。
(1)实现replacer:
public class FXNewsProviderMethodReplacer implements MethodReplacer {
private static final transient Log logger =
LogFactory.getLog(FXNewsProviderMethodReplacer.class);
public Object reimplement(Object target, Method method, Object[] args)
throws Throwable {
logger.info("before executing method["+method.getName()+➥
"] on Object["+target.getClass().getName()+"].");
System.out.println("sorry,We will do nothing this time.");
logger.info("end of executing method["+method.getName()+➥
"] on Object["+target.getClass().getName()+"].");
return null;
}
}
(2)注入replacer,并在主体bean里声明要替换的方法
<bean id="djNewsProvider" class="..FXNewsProvider">
<constructor-arg index="0">
<ref bean="djNewsListener"/>
</constructor-arg>
<constructor-arg index="1">
<ref bean="djNewsPersister"/>
</constructor-arg>
<!-- 注意这里的声明 -->
<replaced-method name="getAndPersistNews" replacer="providerReplacer">
</replaced-method>
</bean>
<bean id="providerReplacer" class="..FXNewsProviderMethodReplacer">
</bean>
<!-- 其他bean配置 ... -->
如果要替换的方法存在参数,或者对象存在多个重载的方法,可以在<replaced-method>内部通过<arg-type>明确指定将要替换的方法参数类型。
这种方法替换的方式后续会很少用,因为我们有AOP了。
4.4 容器背后的秘密:SpringIOC是怎么实现的
本章的前文讲了SpringIOC怎么用,下面我们讲SpringIOC是怎么实现的。
1.容器启动总览:启动阶段和实例化阶段
SpringIOC容器的作用,如下图,主要是根据配置元数据,绑定整个系统的对象,最终组装成一个基于轻量级容器的应用系统。
SpringIOC的实现主要可分为两个阶段:容器启动阶段和Bean实例化阶段。
为了便于扩展,在上述两个阶段中,又加入了相应的各种容器扩展点。
两阶段示意图:
1.容器启动阶段
该阶段主要是准备工作,主要是收集对象的管理信息,然后附带一些验证性或辅助性工作。
主要流程:
- 通过某途径加载配置元数据Configuration MetaData,xml、注解等方式。
- 用工具类
BeanDefinitionReader
解析加载的配置元数据。形成Bean的定义信息BeanDefinition
,注册到相应的BeanDefinitionRegistry
。
即:配置元数据 –> BeanDefinitionReader
–> BeanDefinition
–>BeanDefinitionRegistry
2.Bean实例化阶段
此时所有的BeanDefinition
都注册到了BeanDefinitionRegistry
。
某个请求直接调用容器的getBean()方法,或是根据依赖关系隐式调用到getBean()时,就会激活Bean实例化阶段。
主要流程:
- 先检查所请求的对象之前是否已经初始化。
- 若没有,则根据注册的
BeanDefinition
实例化被请求对象,并为其注入依赖。 - 实例化完毕后,若该对象实现了某些回调接口,则继续根据回调接口的要求来装配它。
- 对象装配完毕后,容器立即将其返回请求方使用。
2.容器启动阶段的扩展点:BeanFactoryPostProcessor
(1)用途
Spring提供了BeanFactoryPostProcessor
,让我们对已经注册到容器的BeanDefinition
进行修改。
相当于在容器实现的第一阶段最后加入一道工序,让我们对最终的BeanDefinition
做一些额外的操作,比如修改其中bean定义的某些属性,为bean定义增加其他信息等。
(2)如何使用
如何使用BeanFactoryPostProcessor:
- 实现
org.springframework.beans.factory.config.BeanFactoryPostProcessor
接口 - 同时实现
org.springframework.core.Ordered
接口:因为可能有多个处理器,需要明确PostProcessor的执行顺序。
(3)常用的实现类
Spring已经提供了一些现成的BeanFactoryPostProcessor
实现类,供我们使用:
- org.springframework.beans.factory.config.
PropertyPlaceholderConfigurer
:属性变量赋值,解析xml文件中的占位符。 - org.springframework.beans.factory.config.
PropertyOverrideConfigurer
:属性覆盖。 - org.springframework.beans.factory.config.
CustomEditorConfigure
:注册自定义的属性编辑器。
(4)两种容器的不同使用方式
BeanFactory容器和ApplicationContext容器使用方式不太一样:
BeanFactory:需要手动代码应用所有的
BeanFactoryPostProcessor
。// 声明将被后处理的BeanFactory实例 ConfigurableListableBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("...")); // 声明要使用的BeanFactoryPostProcessor PropertyPlaceholderConfigurer propertyPostProcessor = new PropertyPlaceholderConfigurer(); propertyPostProcessor.setLocation(new ClassPathResource("...")); // 关键步骤:执行后处理操作 propertyPostProcessor.postProcessBeanFactory(beanFactory);
ApplicationContext:会自动识别配置文件中的
BeanFactoryPostProcessor
并应用它。我们只需要配置注入即可。<beans> <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>conf/jdbc.properties</value> <value>conf/mail.properties</value> 10 </list> </property> </bean> </beans>
(5)详细介绍常用的3个BeanFactoryPostProcessor
实现类
1.PropertyPlaceholderConfigurer
作用:用来解析xml文件中的占位符,变更BeanDefinition中的数据。
比如以下xml配置:
<bean id="dataSource"class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="url">
<value>${jdbc.url}</value>
</property>
<property name="maxActive">
<value>100</value>
</property>
</bean>
jdbc.properties文件配置:
jdbc.url=jdbc:mysql://server/MAIN?useUnicode=true&characterEncoding=ms932&failOverReadOnly=false&roundRobinLoadBalance=true
如果Bean属性使用的是占位符,那么在第一阶段结束时,BeanFactory中保存的对象属性信息还是以占位符的形式存在的。
当PropertyPlaceholderConfigurer
作为BeanFactoryPostProcessor
被应用时,它会用properties文件中的配置信息来替换相应BeanDefinition中的占位符。同时也会参照Java的System类中的Properties。
提供了三种优先级模式:SYSTEM_PROPERTIES_MODE_FALLBACK、SYSTEM_PROPERTIES_MODE_NEVER、SYSTEM_PROPERTIES_MODE_OVERRIDE。默认使用第一种,即配置文件里没有,就到Sytem的Properties中找。
2.PropertyOverrideConfigurer
作用:和前者类似,变更BeanDefinition中的数据。不同的是,它是根据自己的properties文件,直接把bean的属性值覆盖掉,和占位符无关。Bean是无感的。
示例:
前例中的dataSource.maxActive已经赋值为100,完全可以再把它覆盖为200。
# pool-adjustment.properties文件配置
dataSource.maxActive=200
bean注入:
<bean class="org.springframework.beans.factory.config.PropertyOverrideConfigurer">
<property name="location" value="pool-adjustment.properties"/>
</bean>
配置文件里可以存密文:PropertyPlaceholderConfigurer
和PropertyOverrideConfigurer
都继承了PropertyResourceConfigure
。PropertyResourceConfigure
提供了protected的convertPropertyValue()
方法,我们可以在这个方法里对配置项进行转换。例如把密文转换成明文。
3.CustomEditorConfigurer
作用:前面两个BeanFactoryPostProcessor
都是根据配置文件变更BeanDefinition
数据。CustomEditorConfigurer
则没有变更BeanDefinition
,它只是辅助性地将后期会用到的信息注册到容器。
我们知道,Bean的XML文件记录属性是String格式字符串,那么CustomEditorConfigurer其实就是记录了这些字符串到具体对象的转换规则。
要说清CustomEditorConfigurer
的作用,我们先来说下PropertyEditor
的概念:
Spring内部通过JavaBean的PropertyEditor来把字符串转为对象。除了用采用JavaBean框架内默认的PropertyEditor搜寻逻辑来查找PropertyEditor,Spring框架还提供了自身实现的一些PropertyEditor,大概看一下即可:
- StringArrayPropertyEditor:将符合CSV格式的字符串转换成String[]数组的形式。
- classEditor:根据string类型的class名称,直接将其转换成相应的Class对象,相当于class.forName(String)
- FileEditor:对应java.io.File类型的PropertyEditor。
- LocaleEditor:针对java.util.Locale类型的PropertyEditor。
- PtternEditor:针对java.util.regex.Pattern的PropertyEditor。
那么当上述PropertyEditor都不满足需求时,我们就用到了CustomEditorConfigurer
将我们自定义实现的PropertyEditor注册到容器,以便容器遇到某种特定类型时,能找到我们自定义实现的PropertyEditor。
示例,自定义的PropertyEditor使用:
假设需要对yyyy/MM/dd形式的日期格式转换提供支持(当然,Spring提供了CustomDateEditor来支持日期类型的转换,不过这里演示,我们就重写发明轮子啦)。
下面我们就实现针对特定对象类型的PropertyEditor
。说简单点就是,配置中的字符串到某类型怎么转换。
我们需要实现PropertyEditor
接口,直接继承PropertyEditorSuppot也行。
实现类:
// Spring已经提供了PropertyEditor的子类PropertyEditorSuppot,帮我们实现了大量方法,不用我们再去一一实现:
public class DatePropertyEditor extends PropertyEditorSupport{
private String datePattern;
// 覆写该方法即可
// 从String到相应对象类型的转换。如果需要反过来转换,则覆写getAsText()即可。
@Override
public void setAsText(String text) throws IllegalArgumentException {
DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern(getDatePattern());
Date dateValue = dateTimeFormatter.parseDateTime(text).toDate();
setValue(dateValue);
}
public String getDatePattern(){
return datePattern;
}
public void setDatePattern(String datePattern){
this.datePattern= datePattern;
}
}
BeanFactory需要手动注册:
XmlBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource(".."));
//
CustomEditorConfigurer ceConfigurer= new CustomEditorConfigurer();
Map customerEditors = new HashMap();
customerEditors.put(java.util.Date.class,new DatePropertyEditor());
ceConfigurer.setCustomEditors(customerEditors);
//
ceConfigurer.postProcessBeanFactory (beanFactory)
ApplicationContext则会自动识别,我们只需要配置好依赖注入即可:
<bean class="org.springframework.bean.factory.config.CustomEditorCconfiqurer">
<property name="customEditors">
<map>
<entry key= "java.util.Date" bean="datePropertyEditor">
<ref bean="datePropertyEditor"/>
</entry>
</map>
</property>
</bean>
<bean id="datePropertyEditor" class="..DatePropertyEditor" >
<property name ="datePattern">
<value>yyyy</value>
</property>
</bean>
以上是Spring 2.0之前的做法,通过*CustomEditorConfigurer
的customEditors
属性*来指定自定义的PropertyEditor。
Spring2.0之后,提倡用*CustomEditorConfigurer
的propertyEditorRegistrars
属性*来指定自定义的PropertyEditor。需要再增加一个propertyEditorRegistrars实现即可:
public class DatePropertyEditorRegistrar implements PropertyEditorRegistrar {
private PropertyEditor propertyEditor;
public void registerCustomEditors(PropertyEditorRegistry peRegistry) {
peRegistry.registerCustomEditor(java.util.Date.class, getPropertyEditor());
}
public PropertyEditor getPropertyEditor(){ return propertyEditor;}
public void setPropertyEditor(PropertyEditor propertyEditor){
this.propertyEditor = propertyEditor;}
}
注册到容器:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<!-- 注意到是list属性,所以可以把所有的自定义PropertyEditor都放进去 -->
<property name="propertyEditorRegistrars">
<list>
<ref bean="datePropertyEditorRegistrar"/>
</list>
</property>
</bean>
<bean id="datePropertyEditorRegistrar" class="..DatePropertyEditorRegistrar">
<property name="propertyEditor">
<ref bean="datePropertyEditor"/>
</property>
</bean>
<bean id="datePropertyEditor" class="..DatePropertyEditor">
<property name="datePattern">
<value>yyy/M/dd</value>
</property>
</bean>
3.Bean实例化阶段的扩展点
经过容器启动阶段,所有实例化阶段要用到的信息都保存到了BeanDefinition。
此后Bean要等到BeanFactory的getBean()方法被显式调用,或隐式调用时才会发生实例化。
隐式调用根据两种容器分类,有以下两种情况:
- BeanFactory:对象实例化默认是采用延迟初始化。例如A依赖B,对A进行初始化,此时会先隐式初始化依赖项B。
- ApplicationContext:会实例化所有的bean。在启动阶段完成后,会立即*调用所有注册到
ApplicationContext
容器的Bean的实例化方法getBean()
*。也就是说,当我们拿到ApplicationContext
的引用时,所有Bean已实例化完毕。参考:AbstractApplicationContext
的refresh()方法。
某个Bean定义的getBean()
方法第一次被调用时开始实例化阶段,通过createBean()
方法来进行具体的对象实例化。
调用第二次调用时返回第一次实例化的缓存(prototype除外)。
提示:可以在org.springframework.beans.factory.support.AbstractBeanFactory类的代码中查看到getBean()方法的完整实现逻辑,
可以在其子类org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory的代码中一窥createBean()方法的全貌。
这样,Bean的生命周期就被容器管理起来了,不像原来,new完就用,出了作用域就销毁。
Bean的实例化过程,见下图:
1.BeanWrapper
总述:
- 第一步,获取BeanWrapper实例:根据BeanDefinition、CglibSubclassingInstantiationStrategy、bean定义类型获取。
- 第二步,设置Bean属性:用BeanWrapper设置。参考PropertyEditor。使用BeanWrapper方式取代了反射。
(1)第一步,策略模式,获取BeanWrapper实例
容器采用“策略模式”(Strategy Pattern)来决定用哪种方式初始化bean实例:
org.springframework.beans.factory.support.InstantiationStrategy
:实例化策略的总接口。SimpleInstantiationstrategy
:上述接口的子类,实现简单的对象实例化功能,支持反射方式,不支持方法注入方式。CglibSubclassingInstantiationStrategy
:继承了SimpleInstantiationStrategy
的反射方式。增加了方法注入方式,使用的是CGLIB动态字节码生成某个类的子类。
容器默认使用CglibSubclassingInstantiationStrategy
。
(2)第二步,使用BeanWrapper设置Bean属性
BeanWrapper
接口:容器根据BeanDefintion
、CglibSubclassingInstantiationStrategy
、bean定义类型,就可实例化对象。返回的的BeanWrapper
实例 ,它是对实例化对象的包装。
BeanrapperImpl
类:BeanWrapper
接口的实现类。对bean进行包裹,并对该bean进行操作,如设置对象属性。
BeanWrapper接口同时继承了以下接口,从而提供操作bean的能力:
- PropertyAccessor接口:以统一的方式访问对象属性。
- PropertyEditorRegistry接口:启动阶段
CustomEditorConfigurer
将各种PropertyEditor
注册到了容器。然后前面第一步对象构造完成后,Spring会把PropertyEditor
复制一份给BeanWrapperImpl实例,作为其转换类型、设置对象属性的参考。 - TypeConverter接口:类型转换。
这样我们就可以用BeanWrapperImpl
很方便的操作bean实例了,不用再用繁琐的反射API操作。
以下是两种方式操作Bean的对比:
BeanWrapper:
Object provider = Class.forName("package.name.FXNewsProvider").newInstance();
Object persister = Class.forName("package.name.DowJonesNewsPersister").newInstance();
Object listener = Class.forName("package.name.DowJonesNewsListener").newInstance();
BeanWrapper newsProvider = new BeanWrapperImpl(provider);
newsProvider.setPropertyValue("newsListener",listener);
newsProvider.setPropertyWalue("newPersistener",persister);
assertTrue(newsProvider.getWrappedInstance() instanceof FXNewsProvider);
assertSame(provider, newsProvider.getWrappedInstance();
assertSame(listener, newsProvider.getPropertyValue("newsListener"));
assertSame(persister, newsProvider.getPropertyValue("newPersistener"));
反射API:
Object provider = Class.forName("package.name.FXNewsProvider"),newInstance();
Object listener = Class.forName("package.name.DowJonesNewsListener").newInstance();
Object persister =Class.forName("package.name.DowJonesNewsPersister").newInstance();
Class providerClazz= provider.getClass();
Field listenerField= providerClazz.getField("newsListener");
listenerField.set(provider, listener);
Field persisterField = providerClazz.getField("newsListener");
persisterField.set(provider, persister);
assertSame(listener, listenerField.get (provider));
assertSame (persister, persisterField.get(provider));
// 后面还有大量的异常处理程序,比较麻烦...
2.各种Aware接口
Aware接口的使用时机及作用:
- 第一步,对象实例化完成。
- 第二步,相关属性及依赖设置完成。
- 第三步,检查当前对象是否实现了一堆Aware结尾的接口。若实现,则将这些Aware接口中定义的依赖注入到当前对象。
Aware接口有:
- Beafactory容器:
org.springframework.beans.factory.BeanNameAware
:将该对象实例的bean定义对应的beanName设置到当前对象实例。org.springframework.beans.factory.BeanClassLoaderAware
:将对应加载当前bean的Classloader注入当前对象实例。默认使用加载org.springframework.util.ClassUtils类的Classloader。org.springframework.beans.factory.BeanFactoryAware
:BeanFactory容器会将自身设置到当前对象实例。这样,当前对象实例就拥有了一个BeanFactory容器的引用,且可以访问这个容器内允许访问的对象。
- ApplicationContext容器:和BeanFactory不太一样,用的是BeanPostProcessor方式。不过这两步也是相邻的。
org.springframework.context.ResourceLoaderAware 。
:将ApplicationContext容器自身注入到对象实例。因为ApplicationContext实现了Spring的ResourceLoader
接口。org.springframework.context.ApplicationEventPublisherAware
:将ApplicationContext容器自身设置到对象实例。因为ApplicationContext实现了Spring的ApplicationEventPublisher
接接口。org.springframework.context.MessageSourceAware
:将ApplicationContext容器自身设置到对象实例。因为ApplicationContext实现了Spring的Messagesource
接口。用于国际化的信息支持。org.springframework.context.ApplicationContextAware
:将ApplicationContext容器自身设置到对象实例。。
3. BeanPostProcessor
一、概念
与BeanFactoryPostProcessor类似,BeanPostProcessor会处理容器内所有符合条件的实例化后的对象实例。
BeanPostProcessor
声明了两个方法,分别在Bean进行前置处理和后置处理:
postProcessBeforeInitialization()
:前置处理。postProcessAfterInitialization()
:后置处理。
如下:
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws
BeansException;
Object postProcessAfterInitialization(Object bean, String beanName) throws
BeansException;
}
二、BeanPostProcessor
的用途:
- 用来处理标记接口实现类。典型的就是,前面提到的,ApplicationContext的一系列Aware,就是用
BeanPostProcessor
的实现类ApplicationContextAwareProcessor
实现的,通过它的的postProcessBeforeInitialization()
来实现,代码很简单明了:
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ResourceLoaderAware) {
((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
}
if (bean instanceof ApplicationEventPublisherAware) {
((ApplicationEventPublisherAware) bean).setApplicationEventPublisher (this.applicationContext);
}
if (bean instanceof MessageSourceAware) {
((MessageSourceAware) bean).setMessageSource(this.applicationContext);
}
if (bean instanceof ApplicationContextAware) {
((ApplicationContextAware)bean).setApplicationContext(this.applicationContext);
}
return bean;
}
- 对对象实现代理,替换或者字节码增强当前对象实例等:例如AOP使用BeanPostProcessor来为对象生成相应的代理对象。
三、自定义BeanPostProcessor
下面通过自定义的BeanPostProcessor
展示它强大的扩展力。
我们用BeanPostProcessor,对所有的IFXNewsListener的实现类进行统一的解密操作。
操作如下:
- 标注需要解密的实现类,例如定义一个接口去标记。
- 实现一个BeanPostProcessor,判断对象是否符合条件,符合时就进行处理。
- 把实现的BeanPostProcessor注册到容器。
(1)标注需要进行解密的实现类
我们声明了一个接口 PasswordDecodable,用来标记需要对服务器连接密码进行解密的类:
public interface PasswordDecodable {
String getEncodedPassword();
void setDecodedPassword(String password);
}
public class DowJonesNewsListener implements IFXNewsListener,PasswordDecodable {
private String password;
public String[] getAvailableNewsIds() {
// 省略
}
public FXNewsBean getNewsByPK(String newsId) {
// 省略
}
public void postProcessIfNecessary(String newsId) {
// 省略
}
public String getEncodedPassword() {
return this.password;
}
public void setDecodedPassword(String password) {
this.password = password;
}
}
(2)实现相应的BeanPostProcessor对符合条件的Bean实例进行处理
这里就是被PasswordDecodable接口标记的实例。我们从该实例取得加密的密码,然后解密,再把解密后的密码设置回去。这样,它持有的就是解密后的密码了。
public class PasswordDecodePostProcessor implements BeanPostProcessor {
public Object postProcessAfterInitialization(Object object, String beanName)
throws BeansException {
return object;
}
public Object postProcessBeforeInitialization(Object object, String beanName)
throws BeansException {
// 对所有符合条件的对象,进行处理
if(object instanceof PasswordDecodable){
String encodedPassword = ((PasswordDecodable)object).getEncodedPassword();
String decodedPassword = decodePassword(encodedPassword);
((PasswordDecodable)object).setDecodedPassword(decodedPassword);
}
return object;
}
private String decodePassword(String encodedPassword) {
// 实现解码逻辑
return encodedPassword;
}
}
(3) 将自定义的BeanPostProcessor注册到容器
对于BeanFactory容器,我们需要手工编码:调用
CconfigurableBeanFactory
的addBeanPostProcessor()
方法即可:ConfigurableBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource(..)); beanFactory.addBeanPostProcessor(new PasswordDecodePostProcessor()); // getBean();
对于ApplicationContext容器,直接配置注入即可:
<beans> <bean id="passwordDecodePostProcessor" class="package.name.PasswordDecodePostProcessor"> <!--如果需要,注入必要的依赖--> </bean> <!-- .... --> </beans>
扩展:
有一种特殊类型的BeanPostProcessor:org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor
接口:在实例化bean对象步骤之前,容器会检查是否注册有InstantiationAwareBeanPostProcessor
类型的BeanPostProcessor,有的话就会首先使用它来构造对象实例,构造成功后直接返回,不会走正常的实例化流程。
通常都是Spring容器内部使用这种特殊类型的BeanPostProcessor做一些动态对象代理等工作,用户很少会用,这里简单提一下。
4.InitializingBean和init-method
org.springframework.beans.factory.InitializingBean
定义如下:
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
InitializingBean作用:在对象实例化过程调用过“BeanPostProcessor的前置处理”后,若当前对象实现了InitializingBean接口,则会*调用其afterPropertiesSet()
方法进一步调整对象实例状态*。因为有些业务对象在实例化完成后,还要进行一些统一的业务处理,才是可用的。
<bean>的init-method属性:业务对象直接实现InitializingBean接口,显得Spring容器侵入性太强。所以提供了该属性,把方法标注到业务对象里,实现解耦。
使用举例:
开源的库ObjectLabKit,用户类在使用它提供的Datecalculator类进行外汇结算时,需要先向Datecalculator对象提供计算时需要排除的休息日信息。
也就是说,用户类的Bean,需要一个初始化方法,来为Datecalculator类型的工厂,先提供休息日信息。代码如下:
public class FXTradeDateCalculato {
private SqlMapClientTemplate sqlMapClientTemplate;
// ...
public void setupHolidays() {
List holidays = getSystemHolidays();
// ...
LocalDateKitCalculatorsFactory.getDefaultInstance().registerHolidays(holidayKey,holidaysSet);
// ...
}
public List getSystemHolidays() {
return getSqlMapClientTemplate().queryForList("CommonContext.holiday", null);
}
}
方法注册一下:
<beans>
<!-- 注意到这里的init-method方法 -->
<bean id="tradeDateCalculator" class="FXTradeDateCalculator" init-method="setupHolidays">
<constructor-arg>
<ref bean="sqlMapClientTemplate"/>
</constructor-arg>
</bean>
<bean id="sqlMapClientTemplate" class="org.springframework.orm.ibatis.SqlMapClientTemplate">
<!-- ... -->
</bean>
<!-- ... -->
</beans>
极端情况下,比如需要2个先后执行的初始化方法,那么就只有实现InitializingBean
的afterPropertiesSet()
接口,然后在afterPropertiesSet()里调用多个自定义的初始化方法了。
5. DisposableBean与destroy-method
DisposableBean
作用:以上,实例化、注入、设置、调用完成后,容器会检查singleton类型(prototype由用户进行销毁)的bean实例,看其是否实现了org.springframework.beans.factory.DisposableBean
接口,或是是否通过<bean>的destroy-method属性指定了自定义的对象销毁方法。如果是,就为该实例注册一个用于对象销毁的回调(Callback),对象销毁之前,执行该回调方法。
使用举例:
在Spring容器中注册数据库连接池,在系统退出后,连接池应该关闭,以释放相应资源。
<!-- 注意到destroy-method属性 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="url">
<value>${jdbc.url}</value>
</property>
<property name="driverClassName">
<value>${jdbc.driver}</value>
</property>
<property name="username">
<value>${jdbc.username}</value>
</property>
<property name="password">
<value>${jdbc.password}</value>
</property>
...
</bean>
我们需要设置destroy方法执行的时机,不然它不会被调用:
BeanFactory容器:调用
ConfigurableBeanFactory
提供的destroySingletons()
方法销毁容器中管理的所有singleton类型的对象实例。一般我们是在独立应用程序的主程序退出前调用:puble class ApplicationLauncher public static void main(String[] args){ BasicConfigurator.configure(); BeanFactory container = new XmlBeanFactory(new ClassPathResource("..")); BusinessObject bean=(BusinessObject)container.getBean("..."); bean.doSth(); // 注意下面这一行代码 ((ConfigurableListableBeanFactory)container).destroySingletons(); // 应用程序退出,容器关闭 } }
ApplicationContext容器:
AbstractApplicationContext
为我们提供了registerShutdownHook()
方法来完成Singtleton类型的bean对象的销毁。该方法底层是使用标准的Runtime
类的addShutdownHook()
方式来调用相应bean对象的销毁逻辑。当然AbstractApplicationContext
注册的shutdownHook
也包括ApplicationContext相关的事件发布等。public class ApplicationLauncher { public static void main(String[] args) { BasicConfigurator.configure(); BeanFactory container = new ClassPathXmlApplicationContext("..."); // 注意到这一行代码 ((AbstractApplicationContext)container).registerShutdownHook(); BusinessObject bean = (BusinessObject)container.getBean("..."); bean.doSth(); // 应用程序退出,容器关闭 } }
当然,除了Singleton类型的对象,还记得我们前面自定义的Scope么?除了prototype类型,这些类型的Bean也要在适当的时机调用相关对象实例的销毁逻辑。
至此,bean的生命就走到了尽头。
4.5 本章小结
两个阶段两张重要的图:补图。
大概的路径,后续补全:
配置文件 -》 BeanDefinition -》BeanWrapper -》Aware -》BeanPostProcessor -》InitializingBean / init-method -》BeanPostProcessor-》DisposableBean / destory-method
提问
Bean Factory是什么?Spring提供的IOC容器之一,提供最基本的IOC容器服务,默认采用lazy-load,所以启动也快。对象用到的时候才会生成及绑定。
ApplicationContext是什么?Spring提供的另外一个IOC容器。继承至Bean Factory的两个子类,外加扩展另外3个接口。两者关系见图4-2。
怎么使用BeanFactory获取对象?如何把对象注入到BeanFactory?1.在xml配置Bean实现依赖管理,2.初始化BeanFactory
(如BeanFactory container = new XmlBeanFactory(new ClassPathResource(String xmlPath)));
),3.然后直接从BeanFactory
拿bean即可(container.getBean("djNewsProvider");
)
BeanFactory怎么保存对象注册也依赖信息?直接编码、配置文件(xml或properties)、注解
BeanFactory接口?BeanDefinitionRegistry接口?BeanDefinition接口?BeanDefinitionReader接口用来干什么?它们有哪些实现类?DefaultListableBeanFactory
外部配置文件里描述的Bean如何注册到BeanFactory容器里?文件 –> BeanDefinitionReader —BeanDefinition—> BeanDefinitionRegistry –> BeanFactory
想要用自定义格式的文件来描述Bean,怎么做?实现BeanDefinitionReader接口,自己管理文件描述约定、类的加载(?)、BeanDefinition的生成、以及BeanDefinitionRegistry的注册。
注解描述的Bean如何注册到BeanFactory容器里?Bean的扫描注入工作由扫描器完成,在Spring的配置文件里加入并设置好BasePackage。具体原理后续具体分析。
spring的XML文件由哪些元素构成?
beans元素有哪些属性?有哪些元素?各有啥用
bean有哪些属性?
如何表达bean元素之间的依赖?构造方法注入:声明Bean的构造方法参数<constructor-arg>。setter方法注入:声明Bean的属性字段<property>。
有哪些可以用于构造方法注入和setter方法注入的元素类型?几乎都可以呀老铁,null,ref,bean,list,map……
autowire有哪些自动绑定模式?byType和byName有什么区别?其他模式呢?都可以用于构造方法注入和setter方法注入…
bean有哪些作用域?各有什么区别?单例/多例,加上3个专门用于web的作用域。或:对于bean的scope属性,singleton、prototype,request、session、global session各有什么区别?
singleton属性的bean有几个?生命周期?和Singleton模式有啥区别?spring默认的scope是什么?
自定义scope怎么玩?
关键词:BeanDefinitionReader,BeanDefinition,BeanDefinitionRegistry ,BeanFactory,FactoryBean…
静态工厂方法、非静态工厂方法、FactoryBean怎么注入Bean??
怎么用方法注入获取bean?怎么用BeanFactoryAware获取bean?怎么用ObjectFactoryCreatingFactoryBean获取bean?
怎么用org.springframework.beans.factory.support.MethodReplacer接口替换方法逻辑?
设计一个系统,主要有哪些方面?配置类,容器类,工具类,主逻辑。
容器从元数据变成对象,整个流程?经历了哪两个阶段?启动阶段,实例化阶段。
容器启动阶段的扩展点?BeanFactoryPostProcessor
BeanFactoryPostProcessor用来做什么?怎么用?启动阶段的最后一步编辑BeanDefinition。BeanFactory容器手动代码,ApplicationContext容器自动识别。
PropertyPlaceholderConfigurer、PropertyOverrideConfigurer、CustomEditorConfigurer用来做什么?怎么用?PropertyEditor?
两个容器分别什么时候触发Bean的实例化阶段?Bean的实例化过程总览?画图描述
Bean的实例化阶段分哪几步?根据BeanDefiniton生成Wrapper、BeanWrapper设置PropertyEditor并用它设置属性。
配置文件中的字符串,是怎么转换为Bean的具体属性的?策略模式,BeanWrapper,PropertyEditor
Aware接口有啥用?什么时候触发?
BeanPostProcessor接口用来做什么?什么时候触发?
InitializingBean接口用来做什么?什么时候触发?举例?外汇结算排除休息日的业务需求
DisposableBean接口用来做什么?什么时候触发?举例?Singleton销毁时回调,需要手动调用Spring销毁的方法。
第5章 Spring loC容器之ApplicationContext
ApplicationContext
在BeanFactory
功能的基础上主要进行了以下扩展:
- BeanFactoryPostProcessor、BeanPostProcessor以及其他特殊类型bean的自动识别
- 容器启动后bean实例的自动初始化
- 统一的资源加载策略
- 国际化的信息支持
- 容器内事件发布
Spring为基本的*BeanFactory
类型容器提供了XmlBeanFactory
实现*。
相应地,它也为ApplicationContext
类型容器提供了以下几个常用的实现:
org.springframework.context.support.FileSystemXmlApplicationContext
:从文件系统加载bean定义以及相关资源的ApplicationContext实现。org.springframework.context.support.ClassPathXmlApplicationContext
:从Classpath加载bean定义以及相关资源的ApplicationContext实现。org.springframework.context.support.XmlWebApplicationContext
:用于Web应用程序的ApplicationContext实现。后面SpringMVC部分会主要提及。
5.1 统一资源加载策略
Spring提出了一套基于以下两个接口的资源策略:
org.springframework.core.io.Resource
:资源抽象策略,即资源的表示,如何描述资源,包括形式和场合,以及如何访问/交互该资源。org.springframework.core.io.ResourceLoader
资源加载策略,即资源的查找,如何定位/加载资源。
为何Spring要提供新的资源加载策略?
- JavaSE提供的标准类
java.net.URL
描述力不足:即资源的表示不够全面。资源可以是任何形式,如二进制、字节流、文件等。资源可以存在于任何场合,如文件系统、Java应用的Classpath、URL可以定位的地方等。但java.net.URL
只限于基于HTTP、FTP、File等协议的网络形式发布的资源资源定位。 - JavaSE提供的标准类
java.net.URL
职责划分不清:没有把资源的查找和资源的表示划分开。资源查找后,返回的形式多种多样,没有用统一的资源抽象接口进行抽象。
1.描述资源:Resource接口
Spring用*org.springframework.core.io.Resource
接口来描述资源,即资源的表示*,它作为所有资源的抽象和访问接口。
Resource接口可以根据资源的不同类型,或者资源所处的不同场合,给出相应的具体实现。如下是一些实现类:
ByteArrayResource
:将字节(byte)数组提供的数据作为一种资源进行封装。可以通过InputStream形式访问该资源,该资源会根据字节数组的数据,构造相应的ByteArrayInputStream并返回。ClassPathResource
:从Java应用程序的ClasPath中加载具体资源并进行封装。可以指定类加载器,指定要加载的类。FilesystemResource
:对java.io.Fi1e类型的封装。我们可以以文件或者URL的形式访问该类型资源。只要能跟File打的交道,基本上跟FilesystemResource也可以。UrlResource
:通过java.net.URL进行具体资源的查找定位。内部委派URL进行具体的资源操作。InputStreamResource
:将给定的InputStream视为一种资源,较为少用,一般以BytearrayResource以及其他形式资源代替。
上述实现类不够用,还可以自定义资源类型,实现org.springframework.core.io.Resource
接口即可。接口定义如下:
public interface Resource extends InputStreamSource {
boolean exists();
boolean isOpen();
URL getURL() throws IOException;
File getFile() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename();
String getDescription();
}
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
不过Spring给我们提供了org.springframework.core.io.AbstractResource
抽象类,省去了直接实现上述接口的麻烦。
实际使用中,自定义资源的需求几乎没有。
以下是使用现成的实现类ClassPathResource
的一个示例:
BeanFactory beanFactory= new XmlBeanFactory(new ClassPathResource(".."));
...
2.查找定位资源:ResourceLoader
上述给出了资源描述。
Spring提供了org.springframework.core.io.ResourceLoader
接口来查找定位资源。定义如下:
public interface ResourceLoader {
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
// 最重要的方法,根据指定的资源位置,定位到具体的资源实例。
Resource getResource(String location);
ClassLoader getClassLoader();
}
常见实现类:
DefaultResourceLoader
FileSystemResourceLoader
FilesystemxmlApplicationContext
1.实现类:DefaultResourceLoader
即org.springframework.core.io.DefaultResourceLoader
。该类默认按如下顺序查找资源:
- 尝试构建ClassPathResource:首先检查资源路径location是否以*”classpath:”前缀开头*,若是,则构造ClassPathResource类型资源并返回。
- 尝试构建UrIResource:尝试通过URL,根据资源路径来定位资源。若未抛出
MalformedURLException
,则构造UrlResource类型的资源并返回。 - 直接构建ClassPathResource:若以上都失败,则直接调用自己的
getResourceByPath(String)
方法来定位,该方法会构造ClassPathResource类型的资源并返回。
即,如果最终没有找到符合条件的相应资源,getResourceByPath(String)方法就会构造一个实际上并不存在的资源并返回 。
注意到,默认的ResourceLoader
只实现了classpath和url形式,而file形式等未实现,通过后续的实现类去重写方法来实现。
以下是DefaultResourceLoader
使用示例:
ResourceLoader resourceLoader = new DefaultResourceLoader();
// 1.不是classpath:开头,查找失败
// 2.不是url协议开头,查找失败
// 3.默认构建一个实际上并不存在的资源ClassPathResource并返回
Resource fakeFileResource = resourceLoader.getResource("D:/spring21site/README");
assertTrue(fakeFileResource instanceof ClassPathResource); assertFalse(fakeFileResource.exists());// 注意是false
// 检测到url开头,返回UrlResource
Resource urlResource1= resourceLoader.getResource("file:D:/spring21site/README");
assertTrue(urlResourcel instanceof UrlResource);
// 检测到url开头,返回UrlResource
Resource urlResource2= resourceLoader.getResource("http://www.spring21.cn");
assertTrue(urlResource2 instanceof UrlResource);
// ClassPathResource是不存在的
try{
fakeFileResource.getFile();
fail("no such file with path["+fakeFileResource.getFilename()+"] exists in classpath");
}catch(FileNotFoundException e){
//...
}
// 获取成功
try{
urlResourcel.getFile();
}catch(FileNotFoundException e){
fail();
}
2.实现类:FileSystemResourceLoader
为避免返回不存在的资源,此时可用FileSystemResourceLoader
代替。它继承了DefaultResourceLoader
,但覆写了getResourceByPath(String)
方法,使之从文件系统加载资源并以FilesystemResource类型返回。
使用示例如下:
public void testResourceTypesWithFileSystemResourceLoader(){
ResourceLoader resourceLoader = new FileSystemResourceLoader();
Resource fileResource = resourceLoader.getResource("D:/spring21site/README");
assertTrue(fileResource instanceof FileSystemResource);
assertTrue(fileResource.exists());
Resource urlResource = resourceLoader.getResource("file:D:/spring21site/README");
assertTrue(urlResource instanceof UrlResource);
}
3.实现类:FilesystemxmlApplicationContext
类似的,FilesystemxmlApplicationContext
,也是覆写了getResourceByPath(String)
方法的逻辑。
4.批量查找接口:ResourcePatternResolver
该接口继承自ResourceLoader接口。ResourceLoader每次只能返回单个Resource,而ResourcePatternResolver则可以根据指定的资源路径匹配模式,每次返回多个Resource实例。
定义如下:
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
注意到上述新增的"classpath*:"
前缀,以及新增的方法。
最常用的实现类:
org.springframework.core.io.support.PathMatchingResourcePatternResolver
5.实现类:PathMatchingResourcePatternResolver
构建实例时可指定ResourceLoader
,不指定则内部自动构造一个DefaultResourceLoader
实例。
后续资源加载委派给该ResourceLoader
进行处理。加载行为与单独使用ResourceLoader
时基本相同,只是返回数量上的差别。
以下是设定为FileSystemResourceLoader时的使用示例:
public void testResourceTypesWithPathMatchingResourcePatternResolver() {
// 未指定时,使用DefaultResourceLoader
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
Resource fileResource = resourceResolver.getResource("D:/spring21site/README");
assertTrue(fileResource instanceof ClassPathResource);
assertFalse(fileResource.exists()); // 未找到文件形式的Resource
// 指定使用FileSystemResourceLoader
resourceResolver = new PathMatchingResourcePatternResolver(new FileSystemResourceLoader());
fileResource = resourceResolver.getResource("D:/spring21site/README");
assertTrue(fileResource instanceof FileSystemResource);
assertTrue(fileResource.exists()); // 找到了文件形式的Rsource
}
6.总结
整体统一资源加载策略的设计如下图:
3.ApplicationContext与ResourceLoader
ApplicationContext
接口:*ApplicationContext
接口继承了ResourcePatternResolver
接口,而ResourcePatternResolver
接口实现了ResourceLoader
接口*。这就是ApplicationContext
支持Spring内统一资源加载策略的原因。
AbstractApplicationContext
抽象类:所有的ApplicationContext
实现类会直接或者间接地继承org.springframework.context.support.AbstractApplicationContext
。
AbstractApplicationContext
的继承关系:它继承了DefaultResourceLoader
,它的getResource(String)
也就是默认的(支持classpath、url,不支持文件等)。内部的Resource[] getResources(String)
,使用的也是自身即DefaultResourceLoader
。
ApplicationContext
的实现类在作为ResourceLoader
接口或者ResourcePatternResolver
接口时候的工作,完全委派给了PathMatchingResourcePatternResolver
和DefaultResourceLoader
来做。
以下是继承关系图:
1.ApplicationContext扮演ResourceLoader
ApplicationContext
作为ResourceLoader
或者ResourcePatternResolver
来使用:
ResourceLoader resourceLoader = new ClassPathXmlApplicationContext("配置文件路径");
// 或者
// ResourceLoader resourceLoader = new FileSystemXmlApplicationContext("配置文件路径");
Resource fileResource = resourceLoader.getResource("D:/spring21site/README");
assertTrue(fileResource instanceof ClassPathResource);
assertFalse(fileResource.exists());
Resource urlResource2 = resourceLoader.getResource("http://www.spring21.cn");
assertTrue(urlResource2 instanceof UrlResource);
2.Bean注入ResourceLoader
如果我们有一个Bean需要使用ResourceLoader来定位资源,一般我们声明依赖,xml直接注入即可。
不过Spring也提供了API供我们使用,即使用前文中提到的ApplicationContext的特定Aware注入:
- ResourceLoaderAware:注入一个ResourceLoader。
- ApplicationContextAware:ApplicationContext本身就是一个ResourceLoader。
示例如下:
public class FooBar implements ResourceLoaderAware {
private ResourceLoader resourceLoader;
public void foo(String location) {
System.out.println(getResourceLoader().getResource(location).getClass());
}
public ResourceLoader getResourceLoader() {
return resourceLoader;
}
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
public class FooBar implements ApplicationContextAware{
private ResourceLoader resourceLoader;
public void foo(String location){
System.out.println(getResourceLoader().getResource(location).getClass());
}
public ResourceLoader getResourceLoader() {
return resourceLoader;
}
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
this.resourceLoader = ctx;
}
}
<bean id="fooBar" class="...FooBar">
</bean>
ApplicationContext类型容器会自动识别Aware接口,所以直接注入业务Bean即可。
3.Bean注入Resource
Bean定义中的字符串形式信息,可转换成具体对象的依赖类型。
Spring提供了PropertyEditors。不满足的话自定义实现即可。
如果我们要把Resource注入到Bean:
- 对于BeanFacory容器:它不会提供Resource类型对应的PropertyEditor,我们需要把自定义的PropertyEditor注册到容器。
- 对于ApplicationContext容器:它会自动正确识别Resource类型,并转换后注入相关对象。所以直接在xml注入即可,无需提供Resource对应的PropertyEditor。为何能自动识别呢?是因为ApplicationContext启动时,Spring提供的针对Resource类型的PropertyEditor实现
org.springframework.core.io.ResourceEditor
,会通过一个org.springframework.beans.support.ResourceEditorRegistrar
来注册到容器中。
以下为使用示例:
public class XMailer{
private Resource template;
public void sendMail(Map mailCtx){
// String mailContext = merge (getTemplate().getInputStream(),mailctx);
// ..
}
public Resource getTemplate(){ return template;}
public void setTemplate(Resource template) {this.template=template;}
}
<bean id="mailer" class="..XMailer">
<property name="template" value=".resources.default_template.vm"/>
</bean>
4.ApplicationContext的Resource加载行为
Spring扩展了UrlResource,原来UrlResource支持file:、http:、ftp:等,Spring增加了classpath:
和classpath*:
两个前缀。前者用于ResourceLoader,后者用于ResourcePatternResolver。
使用示例:
代码中:
// 代码中使用协议前缀classpath
ResourceLoader resourceLoader = new FileSystemXmlApplicationContext("classpath:conf/container-conf.xml");
配置文件中:
// 配置中使用协议前缀
<bean id="..." class="...">
<property name="...">
<value>classpath:resource/template.vm</value>
</property>
</bean>
对于不同的ApplicationContext,加载Resource的行为不一样。以下举例两个:
ClassPathXmlApplicationContext
:即使没有指明classpath∶或者classpath∶它也会默认从classpath中加载bean定义配置文件。以下代码效果相同:ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml"); ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:conf/appContext.xml");
FileSystemXmlApplicationContext
:默认从文件系统获取,增加classpath:
前缀时,从classpath获取。它与org.springframework.core.io.FileSystemResourceLoader一样,也覆写了DefaultResourceLoader的getResourceByPath(String)方法,逻辑一模一样。// 从文件系统获取 ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml"); // 从classpath获取 ApplicationContext ctx=new FileSystemXmlApplicationContext("classpath:conf/apContext.ml")
// 文件系统 ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml"); // classpath ApplicationContext ctx=new FileSystemXmlApplicationContext("classpath:conf/apContext.ml")
其他还有通配符加载的行为、FileSystemResource的特定行为等,略。
5.2 国际化信息支持
1.JavaSE提供的国际化支持
JavaSE主要提供了两个类来支持国际化:
java.util.Locale
:代表不同的国家地区。java.util.ResourceBundle
:保存特定于某个Locale的信息。
1.Locale
代表不同的国家和地区,包括语言代码以及国家代码,这些代码是ISO标准代码。
构造方法如下:
Locale(String language) ;
Locale(String language, String country) ;
Locale(String language, String country, String variant);
// 举例,以下相当于Locale.CHINA
Locale china = new Locale("zh", "CN");
2.ResourceBundle
保存特定于某个Locale的信息(可以是String类型信息,也可以是任何类型的对象)。
所有的信息序列有统一的一个basename,然后特定的Locale的信息,可以根据basename后追加的语言或者地区代码来区分:
# messages_zh_CN.properties文件中。
# 0是指第一个入参
menu.file=文件({0})
menu.edit=编辑 ·· -
# messages_en_US.properties文件中
menu.file=File({0})
menu.edit=Edit
可通过ResourceBundle的getBundle(String baseName, Locale locale)方法取得不同Locale对应的ResourceBundle,然后再根据资源的键取得相应Locale的资源条目内容。
2.MessageSource与ApplicationContext
一、MessageSource
Spring在Java SE的基础上,进一步抽象了国际化信息的访问接口。即org.springframework.context.MessageSource
,定义如下:
public interface MessagesSource{
String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
String getMessage (String code, Object[] args, Locale locale)throws NoSuchMessageException;
String getMessage(MessagesourceResolvable resolvable, Locale locale)throws NoSuchMessage zException;
}
传入相应的Locale、资源的键以及相应参数,就可以取得相应的信息。三个方法说明如下:
String getMessage(String code, Object[] args, String defaultMessage, Locale locale)
:根据传入的资源条目的键(code参数)、信息参数以及Locale来查找信息。若为找到,则返回defaultMessage。String getMessage(String code, Object[] args, Locale locale) throws NoSuch MessageException
:与第一个方法相同。不过未找到时,抛出错误。String getMessage(MessagesSourceResolvable resolvable,Locale locale)throwsNoSuchessageException
:使用MessagesSourceResolvable对象对资源条目的键、信息参数等进行封装,将Messagesourceresolvable对象作为查询参数来查询。未查到则抛出错误。
二、ApplicationContext
ApplicationContext
除了实现了ResourceLoader以支持统一的资源加载,它还实现了MessageSource
接口。
在默认情况下,ApplicationContext将委派容器中一个名称为messageSource
的MessageSource接口来实现相关功能。没找到该名字的实现,则默认实例化一个不含任何内容的StaticMessageSource。bean配置如下,注意名字为messageSource:
<beans>
<!-- 注意id名字 -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>messages</value> 10 <value>errorcodes</value>
</list>
</property>
</bean>
<!-- ... -->
</beans>
配置完之后就直接可以访问信息了:
ApplicationContext ctx = ...;
String fileMenuName = ctx.getMessage("menu.file", new Object[]{"F"}, Locale.US);
String editMenuName = ctx.getMessage("menu.file", null, Locale.US);
assertEquals("File(F)", fileMenuName);
assertEquals("Edit", editMenuName);
1.Spring提供的MessageSource实现
Spring提供了三个实现,都可独立使用,不必依赖于ApplicationContext:
org.springframework.context.support.StaticMessageSource
:简单实现,可通过编程的方式添加信息条目,多用于测试,不应用于生产环境。org.springframework.context.support.ResourceBundleMessagesSource
:基于标准的java.util.ResourceBundle
实现。1.对其父类AbstractMessagesource的行为进行了扩展,提供对多个ResourceBundle的缓存以提高查询速度。2.对参数化的信息和非参数化信息的处理进行了优化,对用于参数化信息格式化的MessageFomat实例也进行了缓存。最常用的、用于正式生产环境下的MessageSource实现。org.springframework.context.support.ReloadableResourceBundleMessageSource
:基于标准的java.util.ResourceBundle
实现。1.通过其cacheseconds属性可以指定时间段,以定期刷新并检查底层的properties资源文件是否有变更。2.可以通过ResourceLoader来加载信息资源文件。使用时,应避免将信息资源文件放到classpath中,因为这无助于定期加载文件变更。
简单使用示例:
StaticMessageSource messageSource= new StaticMessageSource(); messageSource.addMessage("menu.file",Locale.US,"File"); messageSource.addMessage("menu.edit",Locale.US,"Edit"); assertEquals("File(F)", messageSource.getMessage("menu.file",new Object[]{"F"},Locale.US); assertEquals("Edit",messageSource.getMessage("menu.edit",null,"Edit",Locale.US));
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames(new String[]{"conf/messages"});// 从 classpath加载资源文件
assertEquals("File(F)", messageSource.getMessage("menu.file", new Object[]{"F"}, Locale.US));
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames(new String[]{"file:conf/messages"}); // 从文件系统加载资源文件
assertEquals("File(F)", messageSource.getMessage("menu.file", new Object[]{"F"},Locale.US));
*把以上三个MessageSource,选一个配置到ApplicationContext即可,和前文一样,名字为”messageSource”*。
若以上三个仍不能满足,直接继承AbstractMessageSource,甚至直接实现MessageSource接口即可。
2.用MessageSourceAware注入MessageSource
ApplicationContext启动时会自动识别实现了MessageSourceAware接口的bean,所以某个bean要国际化支持的话,直接实现该Aware即可。
不过以上方法显得业务对象对ApplicationContext容器依赖性太强,其实直接注入ApplicationContext
内部的messageSource
即可,不用再去实现Aware。
如下使用实例,有一个通用的Validator数据验证类,需要通过MessageSource来返回相应的错误信息:
public class Validator {
private MessageSource messageSource;
public ValidateResult validate(Object target){
// 执行相应验证逻辑
// 如果有错误,通过messageSource.getMessage(...)获取相应信息并放入验证结果对象中
// 返回验证结果(return result)
}
public MessageSource getMessageSource() {
return messageSource;
}
public void setMessageSource(MessageSource msgSource){
this.messageSource = msgSource;
}
// ...
}
bean配置,直接注入即可,ref为messageSource:
<beans>
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basenames">
<list>
<value>messages</value>
<value>errorcodes</value>
</1ist>
</property>
</bean>
<bean id="validator" class="...Validator">
<property name="messageSource" ref="messageSource"/>
</bean>
</beans>
既然Messagesource可以独立使用,那为什么还让ApplicationContext实现该接口呢?是因为在Web应用程序中,通常会公开ApplicationContext给视图(View)层,这样,通过标签(tag)就可以直接访问国际化信息了。
5.3 容器内部事件发布
ApplicationContext
提供了容器内事件发布功能,基于Java SE的标准自定义事件类实现。
下面先来看下JavaSE的事件发布怎么实现。
1.JavaSE的自定义事件发布
JavaSE的自定义事件发布,主要依赖以下两个:
java.util.EventObject
类:用于扩展所有的自定义事件类型。java.util.EventListener
接口:用于扩展时间的监听器。
自定义事件发布实现方式如下:
- 定义事件:给出自定义事件类型,扩展EventObject即可。
- 定义监听器:实现针对自定义事件类的事件监听器接口。
- 定义发布者:组合事件类和监听器,发布事件。其实就是生产者-消费者模式。
整个Java SE中标准的自定义事件实现就基本上涉及三个角色,即自定义的事件类型、自定义的事件监听器和自定义的事件发布者。其实就是一个“发布-订阅者”模式,关系如下图所示:
以下是详细实现:
(1)给出自定义事件类型(define your own event object)
继承EventObject并扩展即可:
public class MethodExecutionEvent extends EventObject {
private static final long serialVersionUID = -71960369269303337L;
private String methodName;
public MethodExecutionEvent(Object source) {
super(source);
}
public MethodExecutionEvent(Object source, String methodName) {
super(source);
this.methodName = methodName;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
}
(2)实现针对自定义事件类的事件监听器接口(define custom event listener)
事件可在方法开始执行时发布,也可在方法执行即将结束之际发布。所以监听器有两个方法,注意下面是接口:
public interface MethodExecutionEventListener extends EventListener {
// 处理方法开始执行的时候发布的事件
// 注意到绑定的是刚才我们定义的MethodExecutionEvent事件
void onMethodBegin(MethodExecutionEvent evt);
// 处理方法结束时候布的事件
// 注意到绑定的是刚才我们定义的MethodExecutionEvent事件
void onMethodEnd(MethodExecutionEvent evt);
}
然后是实现类:
public class SimpleMethodExecutionEventListener implements MethodExecutionEventListener {
public void onMethodBegin(MethodExecutionEvent evt) {
String methodName = evt.getMethodName();
System.out.println("start to execute the method["+methodName+"].");
}
public void onMethodEnd(MethodExecutionEvent evt) {
String methodName = evt.getMethodName();
System.out.println("finished to execute the method["+methodName+"].");
}
}
(3)定义发布者:组合事件类和监听器,发布事件
// 生产者-消费者模式
public class MethodExeuctionEventPublisher{
private List<MethodExecutionEventListener>listeners = new ArrayList<MethodExecutionEventListener>();
// 被监听的方法
public void methodToMonitor() {
MethodExecutionEvent event2Publish = new MethodExecutionEvent(this,"methodToMonitor");
publishEvent(MethodExecutionStatus.BEGIN,event2Publish);
// 执行实际的方法逻辑
// ...
publishEvent(MethodExecutionStatus.END,event2Publish);
}
// 发布事件方法
protected void publishEvent(MethodExecutionStatus status, MethodExecutionEvent methodExecutionEvent) {
List<MethodExecutionEventListener> copyListeners = new ArrayList<MethodExecutionEventListener>(listeners);
for(MethodExecutionEventListener listener:copyListeners){
if(MethodExecutionStatus.BEGIN.equals(status)) {
listener.onMethodBegin(methodExecutionEvent);
} else {
listener.onMethodEnd(methodExecutionEvent);
}
}
}
// 绑定消费者
public void addMethodExecutionEventListener(MethodExecutionEventListener listener) {
this.listeners.add(listener);
}
// 删除消费者
public void removeListener(MethodExecutionEventListener listener) {
if(this.listeners.contains(listener))
this.listeners.remove(listener);
}
public void removeAllListeners() {
this.listeners.clear();
}
// 测试
public static void main(String[] args) {
MethodExeuctionEventPublisher eventPublisher = new MethodExeuctionEventPublisher();
eventPublisher.addMethodExecutionEventListener(new SimpleMethodExecutionEventListener());
eventPublisher.methodToMonitor();
}
}
主要注意两点:
- 发布事件时注意增删监听器的影响:为避免事件处理期间事件监器的注册或移除影响处理过程,需对事件发布时点的监器列表进行了一个安全复制(safe-copy)。
- 要提供监听器的移除方法:如果没有提供remove事件监器的方法,那么注册的监器实例会一直被引用,即使过期了或者废弃不用了,也依然存在于监器列表中。这会导致隐性的内存泄漏。
2.Spring 的容器内事件发布类结构
Spring容器内事件的各个类如下:
- 事件抽象类:
org.springframework.context.ApplicationEvent
。 - 监听器接口:
org.springframework.context.ApplicationListener
。 - 发布者接口:
ApplicationEventPublisher
。ApplicationContext继承了它。
下面一一做详细介绍:
1.ApplicationEvent
继承自java.util.EvenObject
。有以下三个实现类:
ContextClosedEvent
∶ApplicationContext容器在即将关闭的时候发布的事件类型。ContextRefreshedEvent
∶ApplicationContext容器在初始化或者刷新的时候发布的事件类型。RequestHandledEvent
∶Web请求处理后发布的事件,它有一个子类servletRequestHandledEvent
提供特定于Java E的Servle相关事件。
2.ApplicationListener
继承自java.util.EventListener
。ApplicationContext容器启动时,会自动识别并加载EventListener类型bean定义。
3.ApplicationContext
ApplicationContext
除了继承之前的ResourceLoader
和MessageSource
,还继承了ApplicationEventPublisher
接口。
不过具体实现时,*ApplicationContext
容器的事件发布功能全部委托给了ApplicationEventMmulticaster
接口来做,自己并未亲自实现。容器启动时会检查是否有该接口类型的Bean(),没有的话会自动创建一个SimpleApplicationEventMulticaster
。
以下是继承关系,都是在org.springframework.context.event.*
包下
ApplicationEventMulticaster
接口:具体事件监器的注册管理以及事件发布功能,是由ApplicationEventMulticaster
接口定义。
AbstractApplicationEventMulticaster
抽象类:上述接口的抽象实现类,实现了事件监器的管理。事件发布则委托给了它的子类
SimpleApplicationEventMulticaster
:上述抽象类的一个子类实现,添加了事件发布功能。默认使用了SyncTaskExecutor
进行事件的发布,同步顺序发布。为提高性能,可使用其他类型的TaskExecutor
实现类、
整体关系如下:
3.Spring 容器内事件发布的应用
Spring的事件机制主要是用来在单一容器内使用的,虽然可以配合Remoting实现远程,但是会很奇怪。
下面单一容器内是如何使用的示例 。
首先我们要有发布者,怎么注入呢?
- 使用
ApplicationEventPublisherAware
接口:容器启动时会自动识别Bean是否实现了该接口,然后将ApplicationContext容器本身作为ApplicationEventPublisher注入当前对象,因为ApplicationContext容器本身就是一个ApplicationEventPublisher。 - 使用
ApplicationContextAware
接口:直接使用ApplicationContext当然可以,很好理解。
接下来我们来实现事件发布。用之前的Java原生方式代码改一下。
1.MethodExecutionEvent
就是事件的封装,逻辑基本不用动:
public class MethodExecutionEvent extends ApplicationEvent {
private static final long serialVersionUID = -71960369269303337L;
private String methodName;
private MethodExecutionStatus methodExecutionStatus;
public MethodExecutionEvent(Object source) {
super(source);
}
public MethodExecutionEvent(Object source,String methodName, MethodExecutionStatus methodExecutionStatus) {
super(source);
this.methodName = methodName;
this.methodExecutionStatus = methodExecutionStatus;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public MethodExecutionStatus getMethodExecutionStatus() {
return methodExecutionStatus;
}
public void setMethodExecutionStatus(MethodExecutionStatus methodExecutionStatus) {
this.methodExecutionStatus = methodExecutionStatus;
}
}
2.MethodExecutionEventListener
ApplicationListener
只通过void onApplicationEvent(ApplicationEvent event)
这一个事件处理方法来处理事件。
public class MethodExecutionEventListener implements ApplicationListener {
public void onApplicationEvent(ApplicationEvent evt) {
if(evt instanceof MethodExecutionEvent)
{
// 执行处理逻辑
}
}
}
3.MethodExeuctionEventPublisher
发布者,直接使用aware注入的eventPublisher
来发布事件即可,不用再自己实现发布逻辑:
public class MethodExeuctionEventPublisher implements ApplicationEventPublisherAware {
private ApplicationEventPublisher eventPublisher;
public void methodToMonitor()
{
MethodExecutionEvent beginEvt = new MethodExecutionEvent(this,"methodToMonitor",MethodExecutionStatus.BEGIN);
this.eventPublisher.publishEvent(beginEvt);
// 执行实际方法逻辑
// ...
MethodExecutionEvent endEvt = new MethodExecutionEvent(this,"methodToMonitor",MethodExecutionStatus.END);
this.eventPublisher.publishEvent(endEvt);
}
public void setApplicationEventPublisher(ApplicationEventPublisher appCtx) {
this.eventPublisher = appCtx;
}
}
4.注册到ApplicationContext容器
将MethodExeuctionEventPublisher
和MethodExecutionEventListener
注册到ApplicationContext容器即可。
<bean id="methodExecListener" class="..MethodExecutionEventListener"></bean>
<bean id="evtPublisher" class="...MethodExeuctionEventPublisher">
</bean>
通过这一套事件发布机制,我们就可以用来做初始化、监控系统性能等工作了,甚至可以往简单的AOP靠近。
Spring的容器内事件发布机制初要想脱离容器单独使用也不是不可以,直接使用ApplicationEventMulticaster接口进行事件发布即可。
5.4 多配置模块加载的简化
这个功能,只是相对于BeanFactory来说,更好一点。
让容器同时读入划分到不同配置文件的信息。相对于BeanFactory来说,ApplicationContext大大简化了这种情况下的多配置文件的加载工作。
举例:
比如有很多配置文件:
{user.dir}/conf/dao-tier.springxml
{user.dir}/conf/view-tier.springxml
{user.dir}/conf/business-tier.springxml 10 ...
同过Applicationcontext,我们只要以string[]]形式传入这些配置文件所在的路径,即可构造并启动容器:
// ApplicationContext
String[]locations = new String[]{"conf/dao-tier.springxml","conf/view-tier.springxml","conf/business-tier.springxml");
ApplicationContext container = new FileSystemXmlApplicationContext(locations);
// 或者
ApplicationContext container= new ClassPathXmlApplicationContext(locations);
// 甚至于使用通配符
ApplicationContext container= new FilesystemxmlApplicationContext("conf/**/*.springxml");
//而使用BeanFactory来加载这些配置,则需要动用过多的代码,如以下代码所示∶
BeanFactory parentFractory = new XmlBeanFactory(new FileSystemResource("conf/dao-tier.springxml"));
BeanFactory subFactory= new XmlBeanFactory(new FileSystemResource("conf/view-tier.springxml"),parentractory);
BeanFactory subsubFactory = new XmlBeanFactory (new FileSystemResource("conf/business-tier.springxml"),subFactory)
除了可以批量加载配置文件之外,ClassPathXmlApplicationContext还可以通过指定Classpath中的某个类所处位置来加载相应配置文件,配置文件分布结构如下(例子来自Spring参考文档):
com/
foo/
services.xml
daos.xml
MessengerService.class
ClassPathXmlApplicationContext可以通过MessengerService类在Classpath中的位置定位配置文件,而不用指定每个配置文件的完整路径名,如以下代码所示:
ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"}, MessengerService.class);
5.5 小结
AplicationContext有许多BeanFactory所没有的特性,包括统一的资源加载策略、国际化信息支持、容器内事件发布以及简化的多配置文件加载功能。
提问
ApplicationContext在BeanFactory功能的基础上主要进行了哪些扩展?
Spring提供的BeanFactory有哪些?ApplicationContext实现有哪些?
为什么要自己定义资源加载策略?
如何描述资源?有哪些实现类型?
如何定位资源?有哪些实现类?批量定位用哪个?定位的逻辑顺序?ResourceLoader接口,实现类有DefaultResourceLoader、FileSystemResourceLoader、FileSystemXmlApplicationContext、ResourcePatternResolver
AbstractApplicationContext抽象类作为ResourceLoader接口或者ResourcePatternResolver接口时,使用的具体是哪两个类?见继承关系图
对于两个容器,Bean如何注入ResourceLoader、Resource?
不同ApplicationContext实现的的Resource的加载行为有何不同?
JavaSE用哪两个类提供国际化支持?Spring呢?ApplicationContext实现了哪个MessageSource?为何要实现?
ApplicationContext底层的事件发布原理?如何实现JavaSE的自定义事件发布?发布订阅者模式,EventObject、EventListener。
Spring容器内的事件发布,涉及哪些类?如何进行应用?见5.3
第6章 Spring IoC容器之扩展篇
6.1 Spring 2.5 的基于注解的依赖注入
6.1.1 自动绑定 + @Autowired
1.使用@Autowired取代xml配置
直接在xml文件配置自动绑定,之后直接使用注解标记即可。绑定方式可以在beans统一指定,也可以在每个bean里指定:
<beans default-autowire="byType">
<bean id="newsProvider" class="..FXNewsProvider" autowire="byType"/>
<bean id="djNewsListener" class="..DowJonesNewsListener"/>
<bean id="djNewsPersister" class="..DowJonesNewsPersister"/>
</beans>
@Autowired可以用在以下位置:
- 属性字段上:注入属性字段。
- 构造方法上:此时是用于自动注入构造方法入参。
- 普通方法上:setter方法可以放,普通方法也可以放。同样的,也是用于自动注入方法入参。
此时注解autowired是取代了原本配置文件里的default-autowire或者autowire配置。上述配置文件就改为:
<!-- 不用再配置autowire -->
<beans>
<bean id="newsProvider" class="..FXNewsProvider"/>
<bean id="djNewsListener" class="..DowJonesNewsListener"/>
<bean id="djNewsPersister" class="..DowJonesNewsPersister"/>
</beans>
此时相当于是遍历每个bean,根据注解取到需要设置autowired的字段、构造方法、普通方法。原型代码如下:
Object[] beans = ...;
for(Object bean:beans) {
if(autowiredExistsOnField(bean)) {
Field f = getQulifiedField(bean));
setAccessiableIfNecessary(f);
f.set(getBeanByTypeFromContainer());
}
if(autowiredExistsOnConstructor(bean)) {
... }
if(autowiredExistsOnMethod(bean)) {
...
}
}
反射取注解的功能由BeanPostProcessor
实现:上面的功能通过org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
来实现,当然,需要用ApplicationContext
类型的容器支持。把这个bean加入到容器即可:
<!-- 注意AutowiredAnnotationBeanPostProcessor的bean配置 -->
<beans>
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>
<bean id="newsProvider" class="..FXNewsProvider"/>
<bean id="djNewsListener" class="..DowJonesNewsListener"/>
<bean id="djNewsPersister" class="..DowJonesNewsPersister"/>
</beans>
目前有点尴尬,配置一半放在注解里,一般放在xml文件里。后面会解决该问题。
2.@Qualifier注解
@Qualifier用来做什么:@Autowired是按照类型进行匹配,如果根据类型找到多个同类型的实例,此时需要用@Qualifier标明。
@Qualifier注解实际上就是byName自动绑定的注解版。
举例如下,IFXNewsListener的两个实现类:
<beans>
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>
<bean id="newsProvider" class="..FXNewsProvider"/>
<bean id="djNewsListener" class="..DowJonesNewsListener"/> <!-- IFXNewsListener实现类 -->
<bean id="reutersNewsListner" class="..ReutersNewsListener"/> <!-- IFXNewsListener实现类 -->
<bean id="djNewsPersister" class="..DowJonesNewsPersister"/>
</beans>
使用时只需用@Qualifier指明即可:
public class FXNewsProvider {
@Autowired
@Qualifier("reutersNewsListner") // 标明使用哪个即可
private IFXNewsListener newsListener;
@Autowired
private IFXNewsPersister newPersistener;
//...
}
@Qualifier同样可以用在构造方法和普通方法上:
public class FXNewsProvider {
//...
@Autowired
public void setUp(@Qualifier("reutersNewsListner") IFXNewsListener newsListener, IFXNewsPersister newPersistener) {
this.newsListener = newsListener;
this.newPersistener = newPersistener;
}
//...
}
6.1.2 @Resource注解
bean注入有两套注解:
- Spring提供的方案:
@Autowired
和@Qualifier
- JSR250:
@Resource
、@PostConstruct
、以及@PreDestroy
@Resource是byName自动绑定。
能标记的地方与@Autowired大致相同:字段、构造方法、普通方法。
public class FXNewsProvider {
@Resource(name="djNewsListener")
private IFXNewsListener newsListener;
@Resource(name="djNewsPersister")
private IFXNewsPersister newPersistener;
//...
}
用来进行生命周期管理的几对配置方法:
- JSR250:
@PostConstruct
和@PreDestroy
注解,标记在Bean的方法上。 - Spring提供:
InitializingBean
和DisposableBean
接口,实现接口,自动扫描。 - Spring提供:
init-method
和destroy-method
配置项,在xml上配置具体方法。
@Resource的BeanPostProcessor:和@Autowired类似,搭配@Resource的是org.springframework.context. annotation.CommonAnnotationBeanPostProcessor
。把它加到配置即可:
<beans>
<bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor"/>
<bean id="newsProvider" class="..FXNewsProvider"/>
<bean id="djNewsListener" class="..DowJonesNewsListener"/>
<bean id="djNewsPersister" class="..DowJonesNewsPersister"/>
</beans>
<context:annotation-config>
可以统一配置BeanPostProcessor,一举四得:
- AutowiredAnnotationBeanPostProcessor
- CommonAnnotationBeanPostProcessor
- PersistenceAnnotationBeanPostProcessor
- RequiredAnnotationBeanPostProcessor
配置里加上该选项后,就不用再配置以上PostProcessor的bean。且@Resource和@Autowired两套是兼容的:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<!--注意到这里-->
<context:annotation-config/>
<bean id="newsProvider" class="..FXNewsProvider"/>
<!--其他bean定义-->
...
</beans>
6.1.3 classpath-scanning配置
classpath-scanning配置作用:从某一顶层包(base package)开始扫描。当扫描到某个类标注了相应的注解之后,就会提取该类的相关信息,构建对应的BeanDefinition,然后把构建完的BeanDefinition注册到容器。
配置如下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<!--注意到这里-->
<context:component-scan base-package="org.spring21"/>
</beans>
完成的工作:
- 生成BeanDefinition:自动扫描到@Component、@Repository、@Service和@Controller等所有注解,并生成注入bean我们就不用再手动在xml文件里定义bean了。
- 实现context:annotation-config配置:也就是自动注入上述4个BeanPostProcessor。不需要的话,可以把annotation-config属性值从默认的true改为false。
- 个性化扫描:可以通过配置include-filter和exclude-filter等配置项,来定制个性化扫描。
include-filter和exclude-filter可以使用的type类型有annotation、assignable、regex和aspectj四种:
<Beans ...>
<context:component-scan base-package="org.spring21">
<context:include-filter type="annotation" expression="cn.spring21.annotation.FXService"/>
<context:exclude-filter type="aspectj" expression=".."/>
</context:component-scan>
</Beans>
6.2 Spring 3.0展望
类泛型化。
基于注解的工厂方法。
6.3 小结
略。
提问
@Autowired和@Resouce注解什么区别?它们各自搭配的BeanPostProcessor是什么?
@Qualifier用来做什么?
BeanPostProcessor可以怎么统一配置?
第3部分 AOP
第7章 一起来看AOP
7.1 OOP的问题
为什么要有AOP:OOP面向对象很好的实现了业务需求,但是针对系统需求,如统一的日志监控等,OOP则很难实现。于是有了AOP。就这样,OOP和AOP在不同的维度空间相互交叉,完整的实现了整体的需求。
7.2 AOP的实现
AOL概念:与OOP需要语言支持一样,AOP也需要某种语言来实现它的概念实体。这个语言就是AOL(Aspect-Oriented-Language)。
AspectJ是扩展自Java的AOL:AOL可以与系统实现语言相同,也可以不同。比如AspectJ是一种扩展自Java的AOL。
织入(Weave)的概念:就是把AOL组件集成到OOP组件的过程。现实中,需要将AOL实现的各个概念实体,集成到系统语言中,寄生于系统语言上来实现。将AOL组件集成到OOP组件的过程,就叫织入(Weave)。
7.2.1 静态AOP
静态AOP:又称为第一代AOP。最典型的就是AspectJ。静态的意思就是预先编译。
实现方式:响应的逻辑以Aspect形式实现以后(用文件描述),用特定的编译器,把它预先编译并织入系统的静态类中。
AspectJ就是用ajc编译器,把各个Aspect以Java字节码的形式编译到各个功能模块中,以融合Aspect和Class。
静态AOP:
- 优点:性能好。因为JVM是直接加载类运行的,不会有性能损失。
- 缺点:不灵活。有修改的话,需要重新修改Aspect定义文件,然后重新编译Aspect并织入。
7.2.2 动态AOP
动态AOP,又称为第二代AOP。最典型的就是SpringAOP。AspectJ在整合AspectWerkz框架后,也具备了动态织入的能力。所以AspectJ也是Java界唯一同时支持静态AOP和动态AOP的框架。
相比于第一代AOP,第二代AOP的改进有:
- AOL大都采用Java语言实现:AOP的各种概念实体全部都是普通的Java类了。
- AOP是在类加载或系统运行期间动态织入,采用对系统字节码进行操作的方式完成Aspect到系统的织入,而不是预先编译到系统类中。
动态AOP:
- 优点:灵活。
- 缺点:有性能损失。不过随着JVM对反射和字节码技术的优化,这种性能损失可以接受。
7.3 Java平台上的AOP实现机制总结
一般有以下几种实现机制,都经过了相应AOP产品的验证。
7.3.1 源代码织入:Java代码生成
这种方式最早期的EJB容器用的多。现在已经退休不用了。
首先用描述符文件描述织入的信息。然后EJB容器根据这个文件,生成Java代码。通过专用的部署工具或者部署接口生成相应的Java类,再部署对应的EJB模块即可。
7.3.2 编译期织入:AOL扩展
也就是上面说的静态AOP方式。用专门的AOL语言描述切面信息,然后用专用的编译器织入后直接生成字节码。
优点是表达能力强,缺点是不灵活,然后你还要学一门新的AOL语言。
7.3.3 类加载期织入:自定义类加载器
自定义类加载器,通过读取外部文件规定的织入规则和必要信息,在加载class文件的时候,就可以把横切逻辑织入现有逻辑中,然后把改动后的class交给JVM运行。来个偷梁换柱。
通过类加载器,大部分的类和实例都能植入。缺点是,某些应用服务器会控制整个类加载体系(比如Tomcat?),这个时候就会有问题了。
7.3.4 程序运行期织入:动态字节码增强
需要生成子类。
通常class文件使用javac编译器编译而成。但只要符合Java class规范,所以我们也可以用ASM或CGLIB等Java工具库,在程序运行期间,动态构建字节码的class文件。
在程序运行时,我们通过动态字节码增强技术,为相应的类生成子类,然后把横切逻辑织入子类,让程序执行这些动态生成的子类即可。
优点是没有接口也能实现,不足就是必须生成子类。当类或方法声明为final的话,就没法扩展。这个特点恰好与动态代理方式互为补充。
7.3.5 程序运行期织入:动态代理
需要实现接口。
动态代理(Dynamic Proxy)是Java1.3后引入的机制,可以在运行期间,为相应的接口动态生成对应的代理对象。然后我们就可以把逻辑封装到动态代理的InvocationHandler中,然后在系统运行期间,根据要织入的位置,织入到响应的代理类。
动态代理只针对接口有效,所有这种方式需要动态代理方式,被代理的类要实现接口。
SpringAOP默认使用动态代理方式实现动态AOP。在没有接口无法使用动态代理扩展时,会使用CGLIB库的动态字节码增强来实现。
SpingAOP具体的面向切面具体实现,和AspectJ是没有太大关系的,但是,SpringAOP借用了AspectJ的概念。
SpringAOP只对IOC管理的Bean有用。全部类的话要用Spring-AspectJ。(存疑)
7.4 AOP的相关概念
7.4.1 Joinpoint
即执行点。切点。
详细略。
7.4.2 Pointcut
Pointcut概念代表的是Joinpoint的表述方式。横切逻辑织入时,需要参考Pointcut规定的Joinpoint信息,才可以知道往系统的哪些Joinpoint织入横切逻辑。
1.Pointcut表述方式
- 直接指定Joinpoint所在方法。表达简单,用于简单描述。
- 正则表达式。
- 特定的Pointcut表述语言。例如AspectJ提供的。类似正则。
2.Pointcut运算
Pointcut还支持运算。例如and, or等。
7.4.3 Advice
Advice是单一横切关注点逻辑的载体。
可分为:
- Before Advice
- After Advice (After Running, After Returning, After Finally)
- Around Advice
- Introduction:与前面三个不同,它不是根据执行时机来分类。它可以为对象提供新的特性或行为。
7.4.4 Aspect
Aspect是对系统中的横切关注点逻辑进行模块化封装的AOP概念实体。通常Aspect可以包含多个Pointcut以及相关Advice定义。
在Spring AOP中,可以通过@Aspect注解并结合普通的POJO来声明Aspect。
7.4.5 织入和织入器(Weaver)
实现织入的实体。对应AspectJ就是它的ajc编译器。对应Spring AOP来说就是ProxyFactory和Cglib。
7.4.6 目标对象
就是符合Pointcut所指定的条件,被织入横切逻辑的对象。
总体的概念综合起来就是下图:
提问
AOP用来做什么?应用举例?
AOL是什么?AspectJ是什么?织入是什么?
AOP有哪些实现方式?动态静态有什么区别?优缺点?静态AOP,逻辑用AOL语言描述,用特定编译器,以java字节码形式,融合到class文件。动态AOP,类加载或系统运行期间,动态操作字节码。
AOP在哪些时候可以织入?Java代码生成,编译期织入(aol文件+专门的编译器),类加载期(自定义类加载器类加载期),运行期织入(动态字节码增强,动态代理)
AOP有哪些概念?连接点Joinpoint执行点、切入点Pointcut要切入的连接点集合、增强Advice执行逻辑、切面Aspect包含多个Pointcut/Advice、织入。
第8章 Spring AOP概述及其实现机制
8.1 Spring AOP概述
SpringAOP的哲学是用20%的有限支持,满足80%的AOP需求。如果需要更强大的支持,那么SpringAOP也提供了对AspectJ的很好的集成。
SpringAOP的AOL语言是Java,所以在Java的基础上,SpringAOP对AOP的概念进行了适当的抽象和实现,使得每个AOP概念都落到实处。
8.2 Spring AOP的实现机制
动态代理和字节码生成,都是在运行期间为目标对象生成一个代理对象。
所以我们先来了解一下代理模式。
8.2.1 静态代理
简单的说,就是代理类和被代理类实现同一个接口,然后代理类持有被代理类的引用即可。外部访问时,直接访问代理对象。示例:
@Getter
@Setter
public class SubjectProxy implements ISubject {
@Resource
private ISubject subject; // 注入被代理类,构造方法注入或get/set注入
public String request() {
// 可增加访问前逻辑。增加日志记录等,或者直接拒绝访问等
String originalResult = subject.request();
// 可增加访问后逻辑
return "Proxy:" + originalResult;
}
}
// 使用示例
ISubject target = new SubjectImpl();
ISubject proxy = new SujectProxy(target);
proxy.request();
这种静态代理的方式是有缺陷的。比如上述代码,我需要给所有的request()加一个逻辑,那么所有有request()的类都要全部实现一遍静态代理,这样太蛋疼了。所以,我们需要动态代理。
8.2.2 动态代理
JKD1.3之后引入了动态代理。动态代理实际上是JDK在运行期动态创建class字节码并加载的过程。其实就是JDK自动帮我们编写了一个类,(不需要源码,可以直接生成字节码),并不存在可以直接实例化接口的黑魔法
动态代理主要由一个类和一个接口组成,就是java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口。
下面是示例,假设我们要代理两个类,他们的方法0点到6点的时候禁止访问。我们只需要统一实现一个invocationHandler就够了。
首先我们用InvocationHandler
接口来实现我们的增强逻辑:
// 增强逻辑:0点到6点期间不让访问
@Slf4j
public class RequestCtrlInvocationHandler implements InvocationHandler {
private Object target;
public RequestCtrlInvocationHandler(Object target) {this.target = target;}
// 增强逻辑写到invoke()方法里
// 参数:代理对象,所有的方法,方法的入参
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 只增强request()方法
if (!method.getName.equals("request")) {
return null;
}
// 进行增强
TimeOfDay startTime = new TimeOfDay(0, 0, 0);
TimeOfDay endTime = new TimeOfDay(5, 59, 59);
TimeOfDay currentTime = new TimeOfDay();
if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime)) {
log.warn("Service is not available now!");
return null;
}
return method.invoke(target, args);
}
}
然后我们在用Proxy
类,根据RequestCtrlInvocationHandler
的增强逻辑,为ISubject
和IRequestable
两种类型生成相应的代理对象实例即可。
public class Demo {
public void demo() {
// 用InvocationHandler增强生成第一个动态代理对象
ISubject subject = (ISubject)Proxy.newProxyInstance(
ProxyRunner.class.getClassLoader(), // 类加载器
new Class[] {ISubject.class}, // 被代理对象实现的接口
new RequestCtrlInvocationHandler(new SubjectImpl)); // InvocationHandler增强
subject.request();
// 用InvocationHandler增强生成第二个动态代理对象
IRequestable requestable = (IRequestable)Proxy.newProxyInstance(
ProxyRunner.class.getClassLoader(),
new Class[] {IRequestable.class},
new RequestCtrlInvocationHandler(new RequestableImpl()));
requestable.request();
}
}
上述的InvocationHandler
就相当于我们的Advice,我们还可以在InvocationHandler
的基础上细化结构,后续会看到。然后上述动态代理实现的方式是有缺点的,就是被代理的类一定要实现某个接口。
默认情况下,如果SpringAOP发现目标对象实现了相应的Interface
,那么就采用动态代理机制生成代理对象实例。否则,Spring AOP会尝试用CGLIB(Code Generation Library)的开源动态字节码生成类库,为目标生成动态代理的对象。
这也是下一节的内容。
重点总结:用Proxy:newProxyInstance(类加载器,被代理对象接口,InvocationHandler增强)
生成新的代理对象,增强用InvocationHandler
接口的invoke()
方法来封装。
8.2.3 动态字节码生成
实现原理:我们可以对目标对象进行继承扩展,生成它的子类,然后把增强逻辑写到子类方法里,用子类方法覆盖父类的方法。最终系统使用子类来代替目标对象即可。
当然,我们也不可能像静态代理那样,为每一个被代理类生成一个子类。所以我们用到了CGLIB这样的动态字节码生成库。
下面我们来演示一下动态字节码方式是怎么实现的增强的:
假设下面是被代理的类:
public class Requestable{
public void request {
System.out.println("rq in Requestable without implement any interface");
}
}
和动态代理类似,首先第一步,封装增强逻辑到Callback
或MethodInterceptor
接口。封装到子类里需要实现一个net.sf.cglib.proxy.Callback
接口。不过我们一般直接使用net.sf.cglib.proxy.MethodInterceptor
接口就行了,它是CallBack
接口的扩展。
@Slf4j
public class RequestCtrlCallBack implements MethodInterceptor {
public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 只增强request()方法
if (!method.getName.equals("request")) {
return null;
}
// 进行增强
TimeOfDay startTime = new TimeOfDay(0, 0, 0);
TimeOfDay endTime = new TimeOfDay(5, 59, 59);
TimeOfDay currentTime = new TimeOfDay();
if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime)) {
log.warn("Service is not available now!");
return null;
}
return proxy.invokeSuper(object, args);
}
}
然后通过CGLIB的Enhancer
为目标对象动态生成一个子类,并把RequestCtrlCallBack
封装的逻辑织入子类即可。
public class Demo {
public void demo() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperClass(Requestable.class);
enhancer.setCallback(new RequestCtrlCallback());
Requestable proxy = (Requestable)enhancer.create();
proxy.request();
}
}
使用CGLIB唯一的限制就是无法对final方法进行覆写。
重点总结:用enhancer
对象设置父类class、callback两个字段后,直接调用create()来生成新的代理对象。实现Callback
或MethodInterceptor
接口的intercept()
方法来封装增强(methodProxy.invokeSuper()
调用原始方法),
提问
静态代理?怎么实现?
动态代理?怎么实现?哪些Api?手动写一个?Proxy.newProxyInstance(类加载器,被代理对象接口,InvocationHandler增强)生成动态代理对象。InvocationHandler接口实现invoke方法。
动态字节码生成?怎么实现?哪些Api?手动写一个?用enhancer对象设置父类class、callback两个字段后,直接调用create()来生成新的代理对象。实现Callback或MethodInterceptor
接口的intercept()方法来封装增强(methodProxy.invokeSuper()调用原始方法)。
第9章 Spring AOP一世
从这一章开始,我们来看SpringAOP具体是怎么做AOP增强的。
Spring2.0前是第一代,之后是第二代。底层实现没怎么变,只是变了各个AOP概念实体的表现形式和AOP的使用方式(注解)。这章我们先来看第一代怎么玩。
我们从各个概念实体来介绍。
9.1 Spring AOP中的Joinpoint
AOP的JoinPoint可以有很多种类型,例如构造方法调用、字段的设置及获取、方法调用、方法执行等。SpringAOP只支持方法执行(Method Execution,这句话非常重要),用20%的功能完成80%的需求。如果有其他需求,Spring AOP提供了对AspectJ的支持。
9.2 Spring AOP中的PointCut接口
1.Pointcut接口
org.springframework.aop.Pointcut
是Spring AOP框架的最顶层抽象。
它定义了两个匹配方法来捕获JoinPoint。分别是getClassFilter()
和getMethodMatcher()
。
然后提供了一个TruePointCut
实例,如果Pointcut的类型是TruePointCut
的话,默认会对系统中所有的对象和所有支持的Joinpoint进行匹配。
Pointcut
接口定义如下:
public interface Pointcut {
ClassFilter getClassFilter(); // 类型匹配。在哪些类生效
MethodMatcher getMethodMatcher(); // 方法匹配。在哪些方法生效
Pointcut TRUE = TruePointcut.INSTANCE; // 不进行过滤,全部方法都生效
}
ClassFilter
接口和MethodMatcher
接口分别是类型匹配和方法匹配,用来匹配被织入的对象和响应的方法。类型匹配和方法匹配分开是为了更灵活。他们之间可以灵活共用,也可以灵活组合,参照后面的ComposablePointcut等。
2.ClassFilter接口
ClassFilter接口:用来进行Class级别的匹配。如果matches()方法返回true,则生效。
当然,如果要排除根据class过滤,那么Pointcut
的getClassFilter可以直接返回TureClassFilter.INSTANCE
,表示所有对象都匹配。
public interface ClassFilter {
boolean matches(Class clazz);
ClassFilter TRUE = TureClassFilter.INSTANCE;
}
该接口使用示例:假如我们只对系统中Foo类型的类进行织入,那么可以这么定义ClassFilter:
public class FooClassFilter {
public boolean matches(Class clazz) {
return Foo.class.equals(clazz);
}
}
3.MethodMatcher接口
用于匹配方法。这个接口比较复杂。定义如下:
public interface MethodMetcher {
boolean matches(Method method, Class targetClass); // 可以不检测方法入参
boolean isRuntime(); // 是否需要参考执行时信息
boolean matches(Method method, Class tartgetClass, Object[] args); // 也可以都检查
MethodMetcher TRUE = TrueMethodMatcher.INSTANCE; // 和上面的匹配所有对象一样的用法
}
可以看到上面有两个matches方法,那么如何决定执行哪个呢?其实是由isRuntime()的返回值来决定。根据isRuntime()返回值的不同,*MethodMatcher
又分成了StaticMethodMatcher
和DynamicMethodMatcher
*,方法的执行顺序也不一样。如下:
- 当
isRuntime()
返回false时,这种类型称为StaticMethodMatcher
:此时无需检查参数,只执行matches(Method method, Class targetClass)
方法即可。由于无需检查参数,那么得到的结果可以缓存到框架内部,可以提高性能。 - 当
isRuntime()
返回true时,这种类型称为DynamicMethodMatcher
:此时需要检查参数。先执行matches(Method method, Class targetClass)
,不匹配则直接返回。匹配的话继续执行matches(Method method, Class tartgetClass, Object[] args)
,结果作为最终结果返回。由于不缓存到框架内部,所以性能差点。
以上。
然后在MethodMatcher
类型的基础上,**Pointcut就可以分为两类,即StaticMethodMetcherPointcut
和DynamicMethodMatcherPointcut
**。
下面是一个不完整的Pointcut族谱,绿框内就是上述MethodMatcher
部分我们的整体描述内容,蓝框则是Spring已经为我们预设好的一些Pointcut,我们可以拿来就用:
9.2.1 常见的Pointcut
上图蓝框中是一些Spring为我们准备好的Pointcut,下面我们就来看下常用的总共有哪些:
Spring预设的StaticMethodMetcherPointcut
总览:
- NameMatchMethodPointcut:根据方法名匹配Joinpoint方法。
- JdkRegexpMethodPointcut:正则表达式匹配。
- Perl5RegexpMethod:正则表达式匹配。
- AnnotationMatchingPointcut:根据是否有指定的注解来匹配Joinpoint方法。分类级别和方法级别。
- ComposablePointcut:计算不同Pointcut的集合,返回结果。
- ControlFlowPointcut:可以指定,由固定的执行者调用时,切点才生效。比较少用。
1.NameMatchMethodPointcut
最简单的Pointcut实现,属于StaticMethodMatcherPointcut的子类。根据自己指定的方法名与Joinpoint处的方法名匹配。用法示例:
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
// 传入方法名,支持通配符
pointcut.setMappedName("mat*ches");
// 也可以传入多个方法名
pointcut.setMappedNames(new String[] {"*matches", "isRuntime"});
由于是静态的StaticMethodMatcherPointcut
,它仅对方法名匹配,无法对重载的方法名进行匹配。
2.JdkRegexpMethodPointcut和Perl5RegexpMethod
这两个也是StaticMethodMatcherPointcut
的子类。都是从AbstractRegexpMethodPointcut
派生出来的,这个抽象类专门用于正则表达式匹配。然后JdkRegexpMethodPointcut
使用jdk自带的正则匹配,Perl5RegexpMethod
则是用perl5格式的正则匹配。
JdkRegexpMethodPointcut
使用示例,Perl5RegexpMethod
的用法类似:
// 匹配的必须是全路径名
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPattern("*.*match.*");
// 或者
pointcut.setPatterns(new String[] {"*.*match.*", ".*matches"});
3.AnnotationMatchingPointcut
用来根据目标中是否存在指定的注解来匹配。注解是JDK1.5发布的。
这个Poingcut的大概意思,就是通过类级、方法级的注解,共同来确定最终的切点。下面详细解释。
切点的定义方法如下:
// 直接new,什么级别则根据注解的级别来
AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(XXAnnotation.class);
// 或者显示指定是类级别还是方法级别:
//用forClassAnnotation()静态方法。和上面等效,但是直接指定了构建的是类级别。
AnnotationMatchingPointcut pointcut = AnnotationMatchingPointcut.forClassAnnotation(ClassLevelAnnotation.class)
// 用forMethodAnnotation()静态方法。和上面等效,但是直接指定了构建的是方法级别。
AnnotationMatchingPointcut pointcut = AnnotationMatchingPointcut.forMethodAnnotation(MethodLevelAnnotation.class)
假设我们指定了以下两个注解:
@Retention(RetentionPolity.RUNTIME)
@Target(ElementType.TYPE) // 类级别
public @interface ClassLevelAnnotation{}
@Retention(RetentionPolity.RUNTIME)
@Target(ElementType.METHOD) // 方法级别
public @interface MethodLevelAnnotation{}
然后假设注解这样使用
@ClassLevelAnnotation
public class GenricTargetObject {
@MethodLevelAnnotation
public void gMethod1() {System.out.println("gMethod1");}
public void gMethod2() {System.out.println("gMethod2");}
}
那么,匹配的行为如下:
切点指定的是类级别注解时:匹配有该注解的类下的,所有方法。例如:
AnnotationMatchingPointcut pointcut = AnnotationMatchingPointcut .forClassAnnotation(ClassLevelAnnotation.class)
;切点指定的是方法级别注解时:匹配所有类中,有这个注解的方法。例如:
AnnotationMatchingPointcut pointcut = AnnotationMatchingPointcut.forMethodAnnotation(MethodLevelAnnotation.class);
也可同时指定类级别和方法级别的注解,从而进行更精确的限制。此时取的是交集。例如:
AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(ClassLevelAnnotation.class, MethodLevelAnnotation.class); // 只会匹配到gMethod1()
注解的配置方式和外部的配置方式并不冲突,还可以有机结合他们。不过现在SpringBoot的趋势是大量的使用注解进行配置。
4.ComposablePointcut
之前我们说过Pointcut是可以计算的。这里的**ComposablePointcut
就是SpringAOP提供用来进行Pointcut之间的计算的。包括并集和交集**两种运算。
并集运算用composablePointcut.union()
,交集运算用composablePointcut.intersection()
,组合运算直接用上述两个方法的链式编程。
示例:
ComposablePointcut pointcut1 = new ComposablePointcut(classFilter1, methodMatcher1);
ComposablePointcut pointcut2 = ...;
// 并集
ComposablePointcut unitedPointcut = pointcut1.union(pointcut2);
// 交集
ComposablePointcut intersectionPointcut = pointcut1.intersection(pointcut2);
// 组合运算
ComposablePointcut pointcut3 = pointcut2.union(classFilter1).intersection(methodMatcher1);
然后如果是简单计算的话,org.springframework.aop.support.Pointcuts
工具类直接提供了Pointcuts.union()
和Pointcuts.intersection()
静态方法进行运算:
ComposablePointcut pointcut1 = ...;
ComposablePointcut pointcut2 = ...;
// 并集
ComposablePointcut unitedPointcut = Pointcuts.union(pointcut1, pointcut2);
// 交集
ComposablePointcut intersectionPointcut = Pointcuts.intersection(pointcut1, pointcut2);
5.ControlFlowPointcut
这个Pointcut类型比较特别,它不是很常用,不过某些场合会用到。
简单的说,之前的Pontcut
是,一旦指定了切点,那么不管是谁调用,切点都是会生效的,符合条件的方法都会被拦截,然后触发织入操作。但是这个ControlFlowPointcut
呢,除了指定了切点以外,还能指定只有被谁调用的时候,这个切点才生效。
性能比较差,需要在运行时检查程序调用栈,且每次都要检查。建议尽量少用。
后续详细略。
9.2.2 自定义Pointcut(Customize Pointcut)
上一小节SpringAOP已经给了我们5个Pointcut,足够使用了。但我们有特殊需要的话,可以自定义Pointcut。
比较爽的是,我们不用从零写。SpringAOP已经给我们提供了抽象类,我们进行扩展就行。
之前讲过,SpringAOP的Pointcut分为StaticMethodMatcherPointcut
和DynamicMethodMatcherPointcut
两个抽象类分支,那么我们在这两个抽象类上实现我们的子类就行了。
1.StaticMethodMarcherPointcut
这个抽象类提供了几个默认实现:
- 不用进行类型匹配:所以getClassFilter()方法直接返回ClassFilter.TRUE。如果子类要做限制的话,可以通过public void setClassFilter(ClassFilter classFilter)方法设置。
- isRuntime()返回false:由于是静态的嘛,很好理解。
- 三个参数的mathes方法直接抛异常:因为是静态的,不涉及方法参数。
所以,我们只需要实现两个参数的matches()方法就行了。
示例:捕捉DAO层中所有的getXXX方法:
public class GetMethodPointcut extends StaticMethodMarcherPointcut {
public boolean matches(Method method, Class clazz) {
return method.getName.startswith("get")
&& clazz.getPackage.getName().startswith("...dao")
}
}
2.DynamicMethodMatcherPointcut
也是提供了默认实现:
- 不用进行类型匹配:所以getClassFilter()方法返回ClassFilter.TRUE,和上面一样。如果子类要做限制的话,可以通过public void setClassFilter(ClassFilter classFilter)方法设置。
- isRuntime()返回true:由于是动态的嘛,很好理解。
- 两个参数的matches()方法默认返回true:不过我们愿意的话也可以覆写下,这样就可以提前在三个参数的mathes()方法之前进行检查。
使用示例:
// 入参为"12345"时才进行拦截
public class PKeySpecificQueryMethodPointcut extends DynamicMethodMatcherPointcut {
public boolean matches(Method method, Class clazz, Object[] args) {
if (method.getName().startsWith(*get*)
&& clazz.getPackage().getName.startsWith("...dao")) {
if (!ArrayUtils.isEmpty(args)) {
return StringUtils.euquals("12345", args[0].toString())
}
return false;
}
}
}
9.2.3 IoC容器中的Pointcut
Pointcut实现的bean可以注入:因为Spring中的Pointcut实现都是普通对象,所以我们都可以通过IOC容器来注册使用它们。
某个Pointcut需要的依赖,可以通过IoC注入。或者某个对象需要依赖pointcut,也可以把这个pointcut注入到依赖对象中。
不过一般不会这么玩,不会直接把某个pointcut注入到容器,然后公开的给容器里面的对象使用。这个稍后说。
这里只是强调,将各个pointcut以独立的形式注册到容器中使用是完全合理的。如下:
<bean id="nameMatchPointcut" class="org.springframeword.aop.support.NameMatchMethodPointcut">
<property name="mappedNames">
<list>
<value>methodName1</value>
<value>methodName2</value>
</list>
</property>
</bean>
9.3 Spring AOP中的Advice
Spring AOP加入了开源组织AOP Alliance,所以Spring中的Advice全部遵循AOP Alliance规定的接口。下图为这些接口的关系:
Advice分类:在Spring中,Advice按照它自身的对象是否能在目标对象类的所有实例中共享,分为了per-class
和per-instance
两大类。
9.3.1 per-class类型的Advice
这种Advice的实例,可以在目标对象类的所有实例之间共享。一般这种Advice只提供方法拦截功能,不会为目标对象类保存状态,也不为对象添加新特性。
上图中的Advice全部都是per-class类型。这些是最常见的Advice类型。唯一的例外就是,不在上图中的Introdution类型的Advice。
下面我们对这些per-class类型的Advice来详细解释:
1.Before Advice
最简单的Advice,只是标志接口,没有定义方法。在Joinpoint之前执行,一般不会打断流程,必要时可通过抛异常来打断流程。
使用的时候,一般我们只要实现MethodBeforeAdvice
即可,定义如下:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before (Method method, Object[] args, Object target) throws Throwable
}
使用示例:假如我们在加载文件资源前,文件不存在的话,指定默认路径:
public class ResourceSetupBeforeAdvice implements MethodBeforeAdvice {
private Resource resource;
public ResouceSetupBeforeAdvice(Resource resource) {
this.resource = resource;
}
public void before (Method method, Object[] args, Object target) throw Throwable {
if (!resource.exists) {
FileUtils.forceMkdir(resource.getFile());
}
}
}
2.ThrowsAdvice
对应AOP概念中的AfterTrowingAdvice。也没有定义方法。不过实现时我们要遵循以下规则:
void afterThrowing(Method m, Object[] args, Object target, ThrowableSubclass t)
其中前三个参数可以省略。然后可以同时实现多个afterThrowing方法来处理不同异常。
示例:
public class ExceptionBarrierThrowAdvice implements ThrowsAdvice {
public void afterThrowing(Throwable t) {//普通异常处理}
public void afterThrowing(RuntimeExcetion t) {//运行异常处理}
public void afterThrowing(Method m, Object[] args, Object target,ApplicationException e) {// 程序生成的异常处理}
}
3.AfterReturningAdvice
定义如下:
public interface AfterReturningAdvice extends AfterAdvice { {
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable;
注意,AfterReturningAdvice是不可以更改返回值的。如有必要更改,可以用后面的AroudAdvice。
4.AroundAdvice
Spring没有定义对应AroundAdvice的实现接口,而是直接采用了AOP Allicance的标准接口,即org.apoalliance.intercept.MethodInterceptor
,定义如下:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
MethodInterceptor可以完成前面所有的Advice功能,还可以完成其他扩展。有很多适用的地方,比如安全验证、性能检测、日志记录等。
示例1:
用来监控系统性能:
public class PerformanceMethodInterceptor implements MethodInterceptor {
private final Log log = LogFactory.getLog(this.getClass);
// MethodInvocation里面可以获取到很多信息
public Object invoke(MethodInvocation invocation) thows Throwable {
StopWatch watch = new StopWatch();
try {
watch.start();
// 直接看做是原方法的调用就行了
return invocation.proceed();
} finally {
watch.stop();
if (log.isInfoEnabled()) {
log.info(watch.toString());
}
}
}
}
示例2:
修改返回值:商品统一打八折
@Setter
@Getter
Public class DiscoutMethodInterceptor implements MethodInterceptor {
private static final Integer DEFAULT_DISCOUT_RATIO = 80;
private static final IntRange RATIO_RANGE = new IntRange(5, 95);
private Integer discountRatio = DEFAULT_DISCOUT_RATIO;
private boolean capaignAvailable;
public Object invoke(MethodInvocation invocation) throws Throwable {
Object returnValue = invocation.proceed();
if (RATIO_RANGE.containsInteger(getDiscoutRation()) && isCampaignAvailable()) {
return ((Integer)returnValue) * getDiscoutRetion()/100;
}
return returnValue;
}
private boolean isCampaignAvailable() {
return campaignAvailable;
}
}
用的时候,直接编程实现,或者用Spring注入都可以,因为Advice都是普通的POJO。
直接编程:
DiscoutMethodInterceptor interceptor = new DiscountMethodInterceptor();
interceptor.setCampaignAvailable(true);
interceptor.setDiscoutRatio(90);
也可以Spring直接注入,XML或者@Bean+@Component方式
<bean id="discountInterceptor" class="...DiscountMethodInterceptor">
<property name="campaignAvailable" value="true" />
<property name="discountRatio" value="90" />
</bean>
9.3.2 per-instance类型的Advice
这种Advice可以理解为对象定制的,和前面的per-class类型有所不同。它会保存不同实例对象的状态和相关逻辑。
在SpringAOP中,Introduction是唯一的一种per-instance型Advice。
Introduction可以在不改动目标类定义的情况下,为目标类添加新的属性和行为。
实现原理是,把新的功能封装成接口,然后用拦截器把接口附加到目标对象上即可。其实就是代理模式。然后这个拦截器就是org.springframework.aop.IntroductionInterceptor
。定义如下:
public interface IntroductionInterceptor extends MethodInterceptor, DynamicIntroductionAdvice {}
public interface DynamicIntroductionAdvice extends Advice {
boolean implementsInterface(Class intf);
}
可以看到,继承了MethodInterceptor
和DynamicIntroductionAdvice
接口。
DynamicIntroductionAdvice
用来指定要应用的接口类有哪些,这些接口类及其实现可以封装新的方法和逻辑等。
MethodInterceptor
用来进行具体的新逻辑整合接入,也就是和之前的per-class一样,实现拦截功能。
然后如果是新增的接口上的方法的话,不用调用MethodInterceptor的proceed()方法,因为不是在老方法上增强的。
下面是总体的Introduction结构图。跟之前的Advice是不一样的:
从图中可以看到,IntroductionInterceptor的Advice分为了两个分支,以IntroductionInfo
为首的静态可配置分支和以DynamicIntroductionAdvice
为首的动态分支。
IntroductionInfo
需要预先定义好要应用到的接口类型,DynamicIntroductionAdvice
正好相反,可到运行时再去判定当前Introduction
要应用到的目标接口类型。
IntroductionInfo
定义如下:
public interface IntroductionInfo {
Class[] getInterfaces(); // 预先指定好要用哪些接口来增强
}
然后我们就可以进行拦截了。拦截的话直接扩展IntroductionInterceptor
接口,在子类的invoke()方法中拦截即可(MethodInterceptor接口的方法)。
不过Spring已经为我们提供了两个现成的实现类可以用了:
- DelegatingIntroductionInterceptor
- DelegatePerTargetObjectIntroductionInterceptor
从名字也能看出,功能是委派(Delegate)给其他类来实现的。
1.DelegationIntroductionInterceptor
下面我们来看下DelegatingIntroductionInterceptor
的用法。
假设我们是开发人员,然后要为我们增加测试的职责。
开发人员:
public interface IDeveloper {
void deveopeSoftware();
}
public class Developer implements IDeveloper {
public void developSoftware() {
System.out.println("I am happy with programming.");
}
}
然后我们为“开发人员”添加新状态或行为。分为以下步骤:
(1)为新状态或行为定义接口。
(2)给出新行为接口的实现类。这个实现类可以随意添加自己想要的方法和逻辑。
(3)通过DelegatingIntroductionInterceptor
来引用(Introduction)拦截
(4)需要注意的是
代码实现:
(1)定义接口:
public interface ITester {
boolean isBusyAsTester();
void testSoftware();
}
(2)实现接口:
@Slf4j
public class Tester implements ITester {
private boolean busyAsTester;
public void testSoftware() {log.info("I testing soft.");}
public boolean isBusyAsTester() {return busyAsTester;}
public void setBusyAsTester (Boolean busyAsTester) {
this.busyAsTester = busyAsTester;
}
}
(3)用DelegatingIntroductionInterceptor
拦截
ITester delegate = new Tester();
DelegationIntrodutionInterceptor intercepter = new DelegationIntruductionInterceptor(delegate);
// 进行织入
ITester tester = (ITester)weaver.weave(developer).with(interceptor).getProxy();
tester.testSoftware();
(4)注意事项的解析:以上,同一个增强功能实例delegate,是被被增强的所有实例共享的。也就是说,虽然每个实例都被增强了,但是增强他们的advice(也就是intercepter)作为一个”黑心”的中介,用的却都是同一个共享的delegate委托对象。所以还是没真正实现per-instance。要实现真正的per-instance的话,需要用到后面的DelegatePerTargetObjectIntroductionInterceptor
。
2.DelegatePerTargetObjectIntroductionInterceptor
与前面不同,它保留了被增强目标对象和相应增强的delegate的映射关系,让被增强对象真正做到“吃自家饭”。
当然,用法是和前面差异不大。不过构造不一样,只需要告诉Class,剩下的交给DelegatePerTargetObjectIntroductionInterceptor
自己去处理就行。这个就是两种方式最大的不同,初始化时,前者传的是delegate实现类的对象,后者则直接传了实现类的接口类的class。
DelegatePerTargetObjectIntroductionInterceptor interceptor = new DelegatePerTargetObjectIntroductionInterceptor(DepegateImpl.class, IDelegate.class);
3.扩展
如果我们想要根据状态加入更丰富的业务,我们也可以对IntroductionInterceptor
进行进一步的扩展。当然,也可以选择扩展现成的两个类,DelegationIntroductionInterceptor
和DelegatePerTargetObjectIntroductionInterceptor
。
不过一般这种扩展可能是因为我们要增强的新功能跟老功能有某种联系。
比如说,前面的开发人员,增加了测试人员的职责,那么直接用DelegationIntroductionInterceptor
实现就好。但是如果我们已经在测试了,就不能再让我们开发,这种新老功能有联系的业务,就可以我们自己来扩展。以下是以DelegationIntroductionInterceptor
作为增强的扩展示例:
public class TesterFeatureIntroductionInterceptor extends DelegatingIntroductionInterceptor implements ITester {
private static final long serialVerdionUID = -3387097483849382081L;
private boolean busyAsTester;
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
if (isBusyAsTest()
&& StringUtils.contains(mi.getMethod().getName(), "developSoftware")) {
throw new RuntimeException("你想累死大爷吗?");
}
return super.invoke(mi);
}
public boolean isBusyAsTester() {return busyAsTester;}
public void setBusyAsTester(boolean busyAsTester) {
this.busyAsTester = busyAsTester;
}
public void testSoftware() {
System.out.println("I will do testing.");
}
}
9.4 Spring AOP中的Aspect
Aspect用来装Pointcut和Advice。
Spring中的Aspect实现是Advisor。它和标准的Aspect有所不同。标准的Aspect定义中可以有多个Pointcut和多个Advice。但是Advisor一般只有一个Pointcut和一个Advice。我们可以认为Advisor是一种特殊的Aspect。
Advisor的实现结构体系可以简单的分为两个分支,org.springframework.aop.PontcutAdvisor
和org.springframework.aop.IntroductionAdvisor
。
9.4.1 PointcutAdvisor家族
实际上,org.springframework.aop.PontcutAdvisor
才是真正定义一个Pointcut和Advice的Advisor。大部分的Advisor实现都是它的“部下”。
下面来看上图中几个常用的PointcutAdvisor实现。
1.DefaultPointcutAdvisor
最通用的PointcutAdvisor实现。除了不能指定Introduction类型的Advice,剩下的任何类型的Pointcut、Advice都可以用DefaultPointcutAdvisor
来整合。
Pointcut和Advice可以在构造DefaultPointcutAdvisor
时指定,也可以后续用setPointcut()和setAdvice()指定。
@Resource
XXXPointcut pointcut; // 任何类型的Pointcut类型
@Resource
XXXAdvice advice; // 除Introduction类型外的任何Advice类型
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
// 或者
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(advice);
advisor.setPointcut(pointcut);
// 或者
DefaultPontcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setAdvice(advice);
advisor.setPointcut(pointcut);
SpriingAOP所有的Bean都是由Spring来管理的。所以,用传统的xml方式也是一样的:
<bean id="pointcut" class="...">
</bean>
<bean id="advice" class="...">
</bean>
<bean id="advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="pointcut" ref="pointcut" />
<property name="advice" ref="advice" />
</bean>
2.NameMatchMethodPointcutAdvisor
相当于细化后的DefalutPointcutAdvisor。只接受NameMatchMethodPointcut的Pointcut类型,不过Advice可以接受除了Introduction外的任意类型。
NameMatchMethodPointcutAdvisor
内部持有了一个NameMatchMethodPointcut
,它的setMappedName()和setMappedNames()方法也就是在操作这个实例。
用法示例,编程方式或是IOC容器注入都可以:
Advice advice = ...; // 除了Introduction的任何Advice类型
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(advice);
advisor.setMappedName("methodName2Intercept");
// 或者
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(advice);
advisor.setMappedNames(new String[] {"method1", "method2"});
IOC使用,注解或XML:
<bean id="advice" class="...">
</bean>
<bean id="advisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<property name="advice">
<ref bean="advice"></ref>
</property>
<property name="mappedNames">
<list>
<value>method1</value>
</list>
</property>
</bean>
3.RegexpMethodPointcutAdvisor
Regexp:Regular expression
这个和前面的NameMatchMethodPointcutAdvisor
类似。RegexpMethodPointcutAdvisor
只支持正则表达式类型的Pointcut。
类似的,也是内部持有一个AbstractRegexpMethodPointcut
实例。
然后我们还记得前文说过,AbstractRegexpMethodPointcut
是有Perl5RegexpMethodPointcut
和JdkRegexpMethodPointcut
两个实现类的。默认RegexpMethodPointcutAdvisor
使用JdkRegexpMethodPointcut
。要强制使用Perl5RegexpMethodPointcut
的话,可以通过setPerl5(boolean)来实现。
和前面一样,我们可以在构造时指定正则表达式匹配模式以及Advice,也可以在后续再指定。
IOC的xml方式使用示例:
<bean id="advice" class="..."></bean>
<bean id="advisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="pattern">
<value>cn\.spring21\..*\.methodNamePattern</value>
</property>
<property name="advice">
<ref bean="advice"></ref>
</property>
<property name="perl5">
<value>false</value>
</property>
</bean>
4.DefaultBeanFactoryPointcutAdvisor
这个用的比较少,而且因为绑定了BeanFactory,所以我们用它时,肯定要绑定到SpringIOC容器。
DefaultBeanFactoryPointcutAdvisor
用来通过容器中Advice注册的beanName来关联Advice,然后在对应的Pointcut匹配成功后,再去实例化对应的Advice。这样从而减少容器启动初期Advisor和Advice之间的耦合。
使用示例:
<bean id="advice" class="..."></bean>
<bean id="pointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedName" value="doSth"></property>
</bean>
<bean id="advisor" class="org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor">
<property name="pointcut" ref="pointcut"></property>
<property name="adviceBeanName" value="advice"></property>
</bean>
注意到对应advice的配置使用属性名来表示的,即“adviceBeanName”,然后它的值就是要绑定的advice的beanName。除了这一点,它和DefaultPointcutAdvisor
的用法是一样的。
9.4.2 IntroductionAdvisor分支
与PontcutAdvisor
最本质的区别,就是它只能应用于类级别的拦截,只能使用Introduction
型的Advice。它纯粹就是为Introduction
而生的。
相对应的,PointcutAdvisor
可以使用任何类型的Pointcut,以及除Introduction
外任意类型的Advice。
IntroductionAdvisor
继承结构比较简单:
既然IntroductionAdvisor
仅限于Introduction
的使用场景,那么DefaultIntroductionAdvisor
也一样的,只能指定Introduction
型的Advice
(也就是IntroductionInterceptor)及其将被拦截的接口类型。
使用示例:
<bean id="introductionInterceptor" class="org.springframework.aop.support.DelegatingIntroductionInterceptor">
<constructor-arg>
<bean class="...DelegateImpl"></bean>
</constructor-arg>
</bean>
<bean id="introductionAdvisor" class="org.springframework.aop.support.DefaultIntroductionAdvisor">
<constructor-arg>
<ref bean="introductionInterceptor" />
</constructor-arg>
<constructor-arg>
<value>...IDelegateInterface</value>
</constructor-arg>
</bean>
我们也可以直接指定Advice和一个IntroductionInfo
对象来构建DefaultIntroductionAdvisor
。因为IntroductionInfo
提供了必要的目标接口类型。示例:
<bean id="introductionInterceptor" class="org.springframework.aop.support.DelegatingIntroductionInterceptor">
<constructor-arg>
<bean class="...DelegateImpl"></bean>
</constructor-arg>
</bean>
<bean id="introductionAdvisor" class="org.springframework.aop.support.DefaultIntroductionAdvisor">
<constructor-arg index="0">
<ref bean="introductionInterceptor" />
</constructor-arg>
<constructor-arg index="1">
<ref bean="introductionInterceptor" />
</constructor-arg>
</bean>
上述代码之,Advisor构造方法所以传入了两个introductionInterceptor
是因为,前者是Introduction
型的advice实例,后者是IntroductionInfo
的实例。因为DelegatingIntroductionInterceptor实现了IntroductionInfo
接口。
9.4.3 Ordered的作用
当多个Advisor的Pointcut匹配到同一个Joinpoint时,这个切点就会执行多个Advice横切逻辑。
默认按Advisor声明的先后顺序排,越往前越优先执行。这个时候例如:权限Advisor的声明顺序在抓取异常的Advisor声明顺序前面,那么权限报错时,抓取异常的切面就抓不到异常,因为它已经在前面执行过了。
这个时候就通过设置各个Advisor的order字段来指定切点的先后执行属性,因为各个Advisor都实现了Ordered接口。
然后order越小,执行顺序越靠前、越优先,即越位于AOP的外侧。例如错误抓取可以排成0,小于0的order一般留给AOP框架自用。
示例,以下设置保证exceptionBarrierAdvisor可以捕获pemissionAuthAdvisor的异常:
<bean id="pemissionAuthAdvisor" class="...PemissionAuthAdvisor">
<property name="order" value="1" />
</bean>
<bean id="exceptionBarrierAdvisor" class="...ExceptionBarrierAdvisor">
<property name="order" value="0" />
</bean>
9.5 Spring AOP的织入
现在各个模块已就绪,就等织入。
AdpectJ用ajc编译器作为织入器;JBoss AOP用自定义的类加载器作为织入器;在Spring AOP中,则使用代理org.springframework.aop.framework.ProxyFactory
作为织入器。
9.5.1 如何使用ProxyFactory
ProxyFactory
并非SpringAOP中唯一可用的织入器,而是最基本的一个实现。它同时支持动态代理和CGLIB。
使用示例:提供目标对象,提供Advisor,织入即可。
ProxyFactory weaver = new ProxyFactory(yourTargetObject);// 第一步
Advisor advisor = ...;
weaver.addAdvisor(advisor); // 第二步
Object proxyObject = weaver.getProxy(); // 第三步
// 现在proxyObject可以使用啦...
使用ProxyFactory
只需要指定如下两个最基本的东西:
- 需要增强的目标对象。可以通过
ProxyFactory
构造方法传入,也可用setter()设置。 - 封装好的切面
Aspect
,在Spring里就是Advisor。
上面第二步,除了指定相应的Advisor
,也可以用weaver.addAdvice(...)
手动指定各种类型的Advice
,指定以后将会根据Advice
生成对应的Advisor
:
- 对于
Introduction
之外的Advice
,ProxyFactory
内部自动构造相应的Advisor
。构造的Advisor中使用的Pointcut
为Pointcut.TRUE
,即相应的Advice
将被应用到系统中所有可识别的Joinpoint
处。 - 对于
Introduction
类型的Advice
,根据该Introduction
的具体类型分情况。a.如果是Introductionlnfo的子类实现,因为共用增强对象,它本身包含了必要的描述信息,框架内部会为其构造一个DefaultIntroductionAdvisor。b.而如果是DynamicIntroductionAdvice的子类实现,框架内部将抛出AopConfigException异常(因为无法从DynamicIntroductionAdvice取得必要的目标对象信息)。
我们也可以指定更多的ProxyFactory
控制属性,来生成必要的代理对象。比如说分别对接口代理和类代理进行区分,采用了动态代理和CGLIB两种机制。
以下举例,在不同的场景下,ProxyFactory
怎么使用,会有一些细微的差异。
需要增强的目标类如下:
// 执行任务
public interface ITask {
void execute(TaskExecutionContext ctx);
}
public class MockTask implements ITask {
public void execute(TaskExecutionContext ctx) {
System.out.printIn("task executed.");
}
}
增强逻辑:
// 记录方法调用时间
public class PerformanceMethodInterceptor implements MethodInterceptor {
private final Log logger = LogFactory.getLog(this.getClass());
public Object invoke(MethodInvocation invocation)throws Tmhrowable {
StopWatch watch= new StopWatch();
try {
watch.start();
return invocation.proceed();
}finally {
watch.stop();
}
if(logger.isInfoEnabled()){
logger.info(watch.toString);
}
}
}
我们来看下,ProxyFactory
怎么进行代理。分为代理实现了接口的类、和代理没有实现接口的类。
1.基于接口的代理
代码实现了接口的类,直接指定接口就行。如MockTask:
MockTask task = new MockTask();
ProxyFactory weaver = new ProxyFactory(task);
// 注意这里,指定了接口。如果不指定,ProxyFactory会自动检测目标类有没有实现接口,只要实现了接口,就会自动按照 面向接口 进行代理。
weaver.setInterfaces(new Class[]{ITask.clasa));
// advisor封装切面,即 切点 + 逻辑
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
advisor.setMappedName("execute")
advisor.setAdvice(new PerformanceMethodInterceptor());
weaver.addAdvisor(advisor);
ITask proxyObject =(ITask)weaver.getProx();
proxyObject.execute(null);
2.基于类的代理
假设目标类如下,没有实现任何接口:
public class Executable{
public void execute(){
System.out.println("Executable without any Interfaces");
}
}
织入如下:
ProxyFactory weaver = new ProxyFactory(new Executable());
// weaver.setProxyTargetClass(true) 可以加这一句,强制使用上述接口方式代理
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
advisor.setMappedName("execute")
advisor.setAdvice(new PerformanceMethodInterceptor();
weaver.addAdvisor(advisor);
Executable proxyObject =(Executable)weaver.getProxy();
proxObject.execute();
System.out.println(proxyObject.getClass());
// 结果:class ...ExecutablesSEnhancerByCGLIBSS9e62fc8:3
将ProxyFactory的optimize属性设定为true的,ProxyFactory也会采用基于类的代理机制。optimize属性后续会说。
总地来说,如果满足以下列出的三种情况中的任何一种,ProxyFactory将对目标类进行基于类的代理:
- 目标类没有实现任何接口:不管proxyTargetClass的值是什么,ProxyFactory会采用基于类的代理。
ProxyFactory
的proxyTargetClass
属性被设置为true:ProxyFactory会采用基于类的代理。ProxyFactory
的optimize
属性被设置为true:ProxyFactory会采用基于类的代理。
3.Introduction型的Adive织入
将他单独列出,是因为Introduction型的Adive比较特别,它用来为已经生成的对象增加行为:
- Introduction可以为已经存在的对象类型添加新的行为:只能应用于对象级别的拦截,而不是通常Advice的方法级别的拦截,所以,进行Introduction的织入过程中,不需要指定Pointcut,而只需要指定目标接口类型。
- Spring的Introduction支持只能通过接口定义为当前对象添加新的行为:所以,我们需要在织入的时机,指定新织入的接口类型。
使用ProxyFactory进行Introduction的织入代码示例如下:
// 对Introduction进行织入,新添加的接口类型必须是通过setInterfaces指定的。
ProxyFactory weaver = new ProxyFactory(new Developer());
weaver.BetInterfaces(new Class[]{IDeveloper.clas8,ITester.class});
TesterFeatureIntroductionInterceptor advice = new TesterFeatureintroductionInterceptor();
weaver.addAdvice(advice);
// 上述直接传入advice。proxyFactory会在自身内部构建相应的Advisor。等效于以下代码:
// DefaultIntroductionAdvisor advisor = new DefaultIntroductionAavisor(advice,adavice);
// weaver.addAdvisor(advisor);
Object prox= weaver.getProxy();
((ITeater)proxy).teatSoftware();
((IDeveloper)proxy).developSoftware(); // 用的是接口
上述是使用接口代理。也可以强制指定使用类代理:
ProxyFactory weaver = new ProxyFactory(new Developer());
weaver.setProxyTargetClass(true); // 注意这里,强制指定用类代理
weaver.getInterfacea(new Class[](ITester.class));// 指定类
TesterFeatureIntroductionInterceptor advice =new TesterFeatureIntroductionInterceptor();
weaver.addAdvice(advice);
//DefaultIntroductionAdvisor advisor = new DefaultIntroductionAavisor(advice,advice);
//weaver.addAdvisor(advisor);
Object proxy = weaver.getProxy();
((ITester)proxy).testSoftware();
((Developer)proxy).develop5oftware(); // 用的是类。和上述不同,使用类代理,将代理对象转型为Developer而不是IDeveloper。
Introduction的Advice以及Advisor是不能跟其他Advice和Advisor混用的,要织入Introduction,你只能使用IntroductionAdvisor或者其子类,而不能使用其他的组合。
9.5.2 ProxyFactory织入器实现
下面我们来看,ProxyFactory是如何实现的。
1.AopProxy接口
接口定义如下:
public interface AopProxy{
Object getProxy();
Object getProxy(ClassLoader classLoader);
}
该接口用来抽象各种不同的代理机制。
2.接口实现类:JdkDynamicAopProxy、Cglib2AopProxy
Spring AOP框架内提供了两种AopProxy实现类:
- 针对JDK动态代理机制:
JdkDynamicAopProxy
。动态代理需要通过InvocationHandler提供调用拦截,所以同时实现了InvocationHandler
。 - 针对CGLIB机制:
Cglib2AopProxy
整个体系如下图所示:
3.接口实例化方式:AopProxyFactory工厂类接口
使用抽象工厂模式来进行实例化:
AopProxyFactory
:根据传入的AdvisedsSupport
实例提供的相关信息,来决定生成什么类型的AopProxy。具体的实例化,交给唯一的实现类:DefaultAopProxyFactory
。
AopProxyFactory接口定义如下:
public interface AopProxyFactory{
AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException;
}
4.工厂接口实现类:DefaultAopProxyFactory
DefaultAopProxyFactory
实现逻辑伪代码:
// 根据传入的Advisedsupport实例,即config所携带的信息,来判断实例化为哪种代理
// config.isOptimize() || config.isProxyTargetclass() || 目标对象没有实现任何接口
if (config.isOptimize() || config.isProxyTargetclass() || hasNoUserSupliedProxyInterfaces(config) {
//创建cglib2AopProxy实例,并返回;
} else {
//创建JdkDynamicAopProxy实例,并返回;
}
5.工厂接口入参封装:AvisedSupport类
下面来看,config入参AvisedSupport
类所承载的信息有哪些。
AdvisedSupport其实就是一个生成代理对象所需要的信息的载体,类图如下:
如上图所示,承载的信息可分为两类:
- 生成代理对象的控制信息:
ProxyConfig
。 - 生成代理对象所需要的必要信息:
Advised
。记载如相关的目标类、Advice、Advisor等。
ProxyConfig
是一个普通的JavaBean,它定义了5个boolean型的属性,信息如下:
- proxyTargetClass:true,使用CGLIB对目标对象进行代理。默认false。
- optimize:代理对象是否需要采取进一步的优化措施。如代理对象生成之后,即使为其添加或者移除了相应的Advice,代理对象也可以忽略这种变动。为true时,使用CGLIB进行代理对象的生成。默认false。
- opaque:生成的代理对象是否可以强制转型为Adavised,默认值为false,表示可以强制转型。
- exposeProxy:Spring AOP框架在生成代理对象时,是否将当前代理对象绑定到hreadLocal。当目标对象需要访问当前代理对象,可以通过Aopcontext.currentProxy()取得。考虑到性能,默认false。
- frozen:设为true时,一旦针对代理对象生成的各项信息配置完成,则不容许更改。这样可以优化代理对象生成的性能。默认false。
Advised
则记录了各种实例化时所需的信息,如:要针对哪些目标类生成代理对象,要为代理对象加入何种横切逻辑等。
默认情况下,Spring AOP框架返回的代理对象都可以强制转型为Advised,用来查询代理对象的相关信息,包括:查询持有的Advisor,添加Advisor、移除Advisor等动作。一般用于测试场景,生产场景不会直接操作Advised。
所以,Advisedsupport的功能如下:
- 继承了ProxyConfig:可通过Advisedsupport设置代理对象生成的一些控制属性。
- 实现了Advised接口:可通过Advisedsupport设置生成代理对象相关的目标类、Advice等必要信息。
6.代理、代理工厂、入参封装:AopProxy、Advisedsupport与ProxyFactory的关系
如下图所示:
简单来说,就是代理工厂ProxyFactory,既可以通过ProxyFactory设置生成代理对象所需的相关信息,又能通过AopProxy取得最终生成的代理对象。
ProxyCreatorSupport
:公用的逻辑抽到里面。
- 自身继承了参数封装类AdvisedSupport:可以拿到生成对象的必要信息。
- 内部持有一个AopProxyFactory实例:可以随时生成对象。默认是DefaultAopProxyFactory。
7.其他织入器实现
除了最基本的ProxyFactory织入器,ProxyCreatorSupport
还继承了其他织入器实现,如下图:
AspectJProxyFactory后面再说。下面我们来看ProxyFactoryBean
。
9.5.3 ProxyFactoryBean织入器实现:容器中的织入器
通过ProxFactory,可以独立于Spring的IoC容器之外来使用Spring的AOP。
在容器里,则可使用ProxyFactoryBean
作为织入器,最大限度的发挥Spring容器的威力。
使用方式与ProxyFactory相差不大。下面来看看它是怎么实现的。
1.ProxyFactoryBean的本质
PoxyFactoryBean
:是Proxy+FactoryBean,而不是ProxyFactory+Bean。也就是说,它本质上是一个用来生产Proxy的FactoryBean。
FactoryBean的作用:如果容器中的某个对象持有某个FactoryBean的引用,则该引用返回的是FactoryBean的getObject()方法所返回的对象,而不是FactoryBean本身。
PoxyFactoryBean的getObject()方法如何返回被代理的对象?由于PoxyFactoryBean继承了ProxCreatorSupport(ProxyFactory也是继承它),所以直接通过父类的createAopProxy()取得相应的Aoprox,然后”return AopProxy.getProxy()”即可。如下所示:
public Object getObject() throws BeansException {
initializeAdvisorChain();
if(isSingleton()) {
return getSingletonInstance();
} else {
if (this.targetName == null) {
logger.warn("Using non-singleton proxies with singleton targets is often undesirable.Enable prototype proxies by setting the 'targetName'property.");
}
return newPrototypeInstance();
}
}
根据 proxyFactoryBean 的 singleton属性不一样,返回的对象也不一样:
- singleton为 true:在第一次生成代理对象之后,会通过内部实例变量singletonInstance(Object类型) 缓存生成的代理对象。
- singleton为 false:proxyFactoryBean每次都会重新检测各项设置,并为当前调用准备一套新的环境,然后再根据最新的环境数据,返回一个新的代理对象。
2.ProxyFactoryBean的使用
ProxyFactory和ProxyFactoryBean都继承了同样的父类,所以大部分设置项都相同。例如设置基于接口还是基于类代理。
ProxyFactoryBean
特有的设置项如下:
- proxyInterfaces:它是一个Collection。1.如果采用接口代理,那么需要把接口类型配到这个属性上。2.这与通过Interfaces属性指定接口类型是等效的。3.当然,即使我们不通过该属性指定,ProxyFactroyBean也可以自动检测到目标对象所实现的接口。因为它有一个autodetectInterfaces属性,默认为true。即如果没有明确指定要代理的接口类型,是否自动检测目标对象所实现的接口类型并进行代理。
- interceptorNames:它是一个Collection。用于指定多个将要织入到目标对象的Advice、拦截器以及Advisor。就不用再像ProxyFactory那样用addAdvice或者addAdvisor方法一个一个添加。该属性还有两个特性:
- 可指定目标对象:如果没有通过相应的设置目标对象的方法明确为ProxyFactoryBean设置目标对象,那么可以在interceptorNames的最后一个元素位置,放置目标对象的bean定义名称。这是个特例,大部分情况下,还是建议明确指定目标对象,而避免这种配置方式。
- *通配符:通过在指定的interceptorNames某个元素名称之后添加*通配符,可以让ProxyFactoryBean在容器中搜寻符合条件的所有的Advisor并应用到目标对象。如,global*代表名字为global开头的所有advisor。
- singleton:每次getobject()调用是否返回新对象。默认true。使用有状态的代理时再设置为false,如Introduction。
现在,从Pointcut到Advice再到Advisor,从目标对象到相应的代理对象,全部都可以由IoC容器统一管理起来了。
1.普通目标对象的织入
即通过ProxyFactoryBean生成目标对象的代理对象,正常的配置ProxyFactoryBean即可:
<bean id="pointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedName"value="execute"/>
</bean>
<bean id="performanceInterceptor" class="..advice.PerformanceMethodInterceptor">
</bean>
<bean id="performanceAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="pointcut">
<ref bean="pointcut"/>
</property>
<property name="advice">
<ref bean="performanceInterceptor"/>
</property>
</bean>
<bean id="task" class="...MockTask">
</bean>
<bean id="taskProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref bean="task"/>
<!-- 为了目标bean被误用,也可以只声明到这里,其他地方就不声明了。如下所示,注意到不是ref了 -->
<!-- <bean clas8="..,MockTask"/> -->
</property>
<property name="proxyInterfaces">
<list>
<value>...ITask</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>performanceAdvisor</value>
</list>
</property>
</bean>
<!-- 因为autodetectInterfaces默认值为true,如果确认目标对象实现的接口就是要代理的接口,那么可以省略 proxyInterfaces 和 interceptorNames 的声明 --><bean id="taskProx" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target"> <bean class="...MockTask"/>
</property> <property name="interceptorNames"> <list>
<value>performanceAdvisor</value>
</1ist>
</property>
</bean>
<!-- 没有指定代理类型,且目标对象也没有实现接口,那么会改为基于类来代理 -->
<!-- 当然,即使实现了接口,也可以强制改为基于类来代理,如下所示 -->
<bean id="taskProxy"class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<bean clasSs="...MockTask"/>
</property>
<property name="proxyTargetClass"><!-- 强制指定类代理。此时得到的代理对象,只能强制转换为它的父类,而不是接口 -->
<value>true</value>
</property>
<property name="interceptorNames">
<list>
<value>performanceAdvisor</value>
</1ist>
</property>
</bean>
2.Introduction的代理
毕竟它比较特殊嘛。单独说下。
以下面的目标对象举例:
public interface ICounter {
void resetCounter();
int getCounter();
}
public class CounterImpl implements ICounter {
private int counter;
public int getCounter(){
counter++;
return counter;
}
public void resetCounter(){
counter = 0;
}
}
需要将ICounter的行为添加到ITask相应的实现类中。配置如下:
<!-- 注意,singleton全部都是"false",即scope全部声明为prototype -->
<!-- 使用的是"taskName"而不是"task"来指定目标对象。如果使用task通过ref指定prototype类型会有问题 -->
<bean id="task" class="..MockTask" singleton="false">
</bean>
<bean id="introducedTask" class="org.springframework.aop.framework.ProxyFactoryBean" singleton="false">
<property name="targetName">
<value>task</value>
</property>
<property name="proxyInterfaces">
<list>
<value>...ITask</value>
<value>..ICounter</value>
</1ist>
</property>
<property name="interceptorNames">
<list>
<value>introductionInterceptor</value>
</1ist>
</property>
</bean>
<bean id="introductionInerceptor" class="org.springframework.aop.support.DelegatingIntroductionInterceptor" singleton="false">
<constructor-arg>
<bean class="...CounterImpl">
</bean>
</constructor-arg>
</bean>
调用代码:
ApplicationContext ctx = new ClassPathXmlApplicationContext(".");
Object proxy1 = ctx.getBean("introducedTask");
Object proxy2= ctx.getBean("introducedTask");
System.out.println(((ICounter)proxy1).getCounter()); // 1
System.out.printIn(((ICounter)proxy1).getCounter()); // 2 每次调用的不是同一个对象
System.out.printIn(ICounter)proxy2).getCounter()); // 1
9.5.4 加快织入的自动化进程
上述,一个一个的手动配置每个目标对象的ProxyFactoryBean,太慢了。
Spring AOP给出了自动代理(AutoProxy)机制,用以帮助我们解决使用ProxyFactoryBean配置工作量比较大的问题。
1.自动代理的实现原理
需要依靠ApplicationContext。原理是依赖IoC容器的BeanPostProcessor
概念。
通过BeanPostProcessor:我们可以在遍历容器中所有bean的基础上,对遍历到的bean进行一些操作。
只需提供一个实现自动化逻辑的BeanPostProcessor即可,BeanPostProcessor实现逻辑:当对象实例化的时候,为其生成代理对象并返回,而不是实例化后的目标对象本身,这样就实现了代理对象的自动生成。伪代码如下:
for (bean in IoC container) {
检查当前bean定义是否符合拦截条件;
如果符合拦截条件,则{
Object proxy = createProxyFor(bean);
return proxy;
} 否则 {
Object instance= createInstance(bean);
return instance;
}
实现方案:
- 代理对象生成:createProxyFor(bean),用ProxFactory或ProxyFactoryBean即可。
- 检查当前bean定义是否符合拦截条件:那么,如何告知具体的自动代理实现类,拦截条件都有哪些:
- 通过外部配置文件传入:如在容器的配置文件中注册有关的Pointcut以及Advisor
- 元数据:即注解方式。
2.可用的AutoProxyCreator
下面来看下有哪些可以使用的自动代理实现类。
Spring AOP在org.springframework.aop.framework.autoproxy
包中提供了两个常用的AutoProxyCreator
:
BeanNameAutoProxyCreator
DefaultAdvisorAutoProxyCreator
1.BeanNameAutoProxyCreator
BeanNameAutoProxyCreator:指定一组容器内的目标对象的beanName,让后将指定的一组拦截器应用到这些目标对象上。
自动代理配置:
<bean id="target1" class="..."/>
<bean id="target2" class="..."/>
<bean id="mockTask" class="..."/>
<bean id="fakeTask"class="..."/>
<bean id="taskThrowsAdvice" class="...TaskThrowsAdvice">
</bean>
<bean id="performanceInterceptor" class="...PerformanceMethodInterceptor">
</bean>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list> <!-- 可以不用list,直接用“,”隔开即可 -->
<value>target1</value> <!-- 名字支持*通配符 -->
<value>target2</value>
</list>
</property> <property name="interceptorNames">
<list>
<value>taskThrowsAdvice</value>
</list>
</property>
</bean>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<value>mockTask</value>
<value>fakeTask</value>
</list> </property> <property name="interceptorNames">
<list>
<value>performanceInterceptor</value>
</list>
</property>
</bean>
2.DefaultAdvisorAutoProxyCreato
比BeanNameAutoProxyCreator自动化程度更高。
用法:只需要在ApplicationContext
的配置文件中注册一下DefaultAdvisorAutoProxyCreator
的bean定义即可。
<bean class="org.apringframework.aop.framework.autoproxy.DefaultAavlLgorAutoProxyCreator"/>
......
DefaultAdvisorAutoProxyCreator
会自动搜寻容器内的所有Advisor,然后根据各个Advisor所提供的拦截信息,为符合条件的容器中的目标对象自动生成相应的代理对象。
DefaultAdvisorAutoProxyCreator只对Advisor有效。因为只有Advisor才同时包含Pointcut和Advice。
由于会对有所Bean生效,所以各个Advisor的定义要足够细化。
3.扩展AutoProxyCreator
以上都不能满足要求,则自己实现。
实现方式:比如基于元数据的方式,我们可以直接接触Spring AOP提供的AbstractAutoProxyCreator
或者AbstractAdvisorAutoProxCreator
,实现相应的子即可。不用重新实现所有逻辑。
继承图如下图所示:
可以看到,所有的AutoProxyCreator都是InstantiationAwareBeanPostProcessor
。
特别的BeanPostProcessor
:当Spring loC容器检测到有InstantiationAwareBeanPostProcessor
类型的BeanPostProcessor
的时候,会直接通过InstantiationAwareBeanPostProcessor
中的逻辑构造对象实例返回,而不会走正常的对象实例化流程。
捎带提一下:AspectJAwareAdvisorAutoProxyCreator是Spring 2.0之后的AutoProxyCreaotr实现,也算是一个AutoProxyCreator的自定义实现。它还有一个子类AnnotationAwareAspectJAutoProxyCreator
,可以支持根据Java5的注解捕获信息以完成自动代理。
9.6 TargetSource
指定目标对象:1.使用ProxyFactory的时候,我们可以用setTarget()方法指定具体的目标对象。2.ProxFactoryBean也是,它还可以用setTargetName()指定bean名称。3.除此之外,还有一种方法指定目标对象,就是setTargetSource()
。
TargetSource是什么:
- TargetSource相当于是目标对象的壳:目标对象被调用时,SpringAOP做了点儿手脚,不是直接调用这个目标对象上的方法,而是通过TargetSource来取得具体目标对象,然后再调用该目标对象上的相应方法。
- 框架会统一用一个TargetSource实现类对目标对象进行封装:不管是通过setTarget(),还是通过setTargetName()等方法设置的目标对象,处理方法都一样。
TargetSource最主要的特性:每次的方法调用都会触发TargetSource的getTarget()方法,getTarget()方法再从相应的TargetSource实现类中取得具体的目标对象,
所以,我们可以设置每次被调用到的目标示例,例如:
- 提供目标对象池:每次从TargetSource取得的目标对象都从这个目标对象池中取得。
- 按某种规则返回:一个TargetSource实现类持有多个目标对象实例,然后按照某种规则,在每次方法调用时,返回相应的目标对象实例。
- 只持有一个对象:通常ProxFactory或者ProxyFactoryBean处理目标对象时,内部会构造一个org.springframework.aop.target.SingletonTargetSource实例。每次方法调用时,SingletonTargetSource都会返回同一个目标对象实例的引用。
1.可用的TargetSource 实现类
来看一下有哪些现成的Targetsource实现类,都放到了于org.springframework.aop.target包中。不能满足时再自定义。
1.singletonTargetSource
使用最多的TargetSource实现类。
在通过ProxyFactoryBean的setTarget()设置完目标对象之后,ProxFactoryBean内部会自行使用一个singletonTargetSource对设置的目标对象进行封装。
内部实现:很简单,就是只持有一个目标对象而已。单例模式。
使用示例:
<bean id="target" class="..."/>
<bean id="singletonTargetSource" class="org.springframework.aop. target.SingletonTargetSource">
<constructor-arg>
<ref bean="target"/>
</constructor-arg>
</bean>
<bean id="targetProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource">
<ref bean="singletonTargetSource"/> <!-- 在这里设置 -->
</property>
<property name="interceptorNames">
<list>
<value>anyInterceptor</value>
</1list>
</property>
</bean>
其实没什么意义,因为跟直接setTarget是等效的。
2.PrototypeTargetSource
每次调用目标对象方法时,PrototypeTargetSource都会返回一个新的目标对象实例供调用。
使用示例:
<bean id="target" class=".." singleton="false"/> <!-- 这里singleton是false -->
<bean id="prototypeTargetSource"class="org.springframework.aop.target.PrototypeTargetSource">
<property name="targetBeanName"> <!-- 通过targetBeanName属性指定目标对象的bean定义名称,而不是引用 -->
<value>target</value>
</property>
</bean>
<bean id="targetProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource"> <!-- 属性名targetSource -->
<ref bean="prototypeTargetSource"/>
</property> <property name="interceptorNames"> <list>
<value>anyInterceptor</value> </1ist>
</property>
</bean>
注意到两点:
- bean定义的 scope 为 prototype 。
- 通过targetBeanName属性指定目标对象的bean定义名称,而不是引用。否则会引用到目标对象,而不是代理对象。
测试代码:
public void testPrototypeTargetSource() throws Exception {
Object proxy = ctx.getBean("targetProxy");
Object targetObject0=((Advised)proxy).getTargetSource().getTarget();
Object targetObject1 =((Advised)proxy).getTargetSource().getTarget();
Object targetObject2 =((Advised)proxy).getTargetSource().getTarget();
assertNotSame(targetObject0,targetObject1);
assertNotSame(targetObject1,targetObject2);
assertNotSame(targetObject0,targetObject2);
}
3.HotSwappableTargetSource
可以让我们在应用程序运行的时候,根据某种特定条件,动态地替换目标对象类的具体实现。
替换方法:用HotSwappableTargetSource的swap方法,可以用新的目标对象实例将旧的目标对象实例替换掉。方法声明如下:
// 返回老的实例
public Object swap(Object newTarget)
要使用HotSwappableargetSource,我们需要在它构造的时候,就提供一个默认的目标对象实例:
<!-- HotSwappableTargetSource的初始化 -->
<bean id="task" class="org.darrenstudio.books.unveilspring.aop.advisor.MockTask"> </bean>
<bean id=-"hotSwapTargetSource"class="org.springframework.aop.target.HotSwappableTargetSource">
<conBtructor-arg> <!-- 构造方法注入 -->
<ref bean="taak"/>
</conBtructor-arg> </bean>
<bean id="taskProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource"ref="hotSwapTargetSource"> </property>
<property name="interceptorNames">
<list>
<value>anyInterceptor</value>
</list>
</property>
</bean>
热替换示例:
// HfotswapableTargetsSource使用示例
// 例如:可以热替换数据源
// ...
Object proxy = ctx.getBean("taskProxy");
Object initTarget =(Advised)proxy).getrargetSource().getTarget();
HotSwappableTargetSource hotSwapTargetSource = (HotSwappableTargetSource)ctx.getBean("hotSwapTargetSource");
Object oldTarget = hotSwapTargetSource.swap(new ITask() {
public void execute(TaskExecutionContext ctx){
//省略
}});
Object newTarget =((Advised)proxy).getTargetSource{).getTarget();
assertSame(initTarget,oldTarget);
assertNotSame(initTarget,newTarget);
4.CommonsPoolTargetSource
提供一个目标对象对象池,然后让某个TargetSource实现每次都从这个目标对象池中取得目标对象。
使用现有的Jakarta CommonsPool提供对象池支持。使用方式和Prototyperargetsource差别不大。
<bean id="target" class=".." aingleton="false"/> <!-- 这里singleton是false -->
<bean id="poolingTargetSource" class="org.springframework.aop.target.CammonsPoolTargetSource">
<property name="targetBeanName"> <!-- 通过targetBeanName属性指定目标对象的bean定义名称,而不是引用 -->
<value>target</value>
</property>
</bean>
<bean id="targetProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="targetSource"> <ref bean="poolingTargetSource"/> </property> <property name="interceptorNames"> <list>
<value>any Interceptor</value> </list>
</property>
</bean>
要注意的地方和Prototyperargetsource一样,两点:
- bean定义的 scope 为 prototype 。
- 通过targetBeanName属性指定目标对象的bean定义名称,而不是引用。否则会引用到目标对象,而不是代理对象。
还有许多控制对象池的可配置属性,比如对象池的大小、初始对象数量等,都可以在配置中指定。
也可以实现AbstractPoolingTargetSource来自定义对象池的TargetSource。
5.ThreadLocalTargetSource
为不同的线程调用提供不同的目标对象。
它可以保证各自线程上对目标对象的调用,可以被分配到当前线程对应的那个目标对象实例上。
非就是对JDK标准的ThreadLocal进行了简单的封装而已。
使用方式与其他TargetSource类似:
<bean id="target" class="..." singleton="false"/> <!-- 这里singleton是false -->
<bean id="threadLocalTargetSource" → Class="org.springframework.aop.target.ThreadLocalTargetSource">
<property name="targetBeanName"> <!-- 通过targetBeanName属性指定目标对象的bean定义名称,而不是引用 -->
<value>target</value>
</property>
</bean> ⑨2 <bean id="targetProxy"class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource">
<ref bean="threadLocalTargetSource"/>
</property> <property name="interceptorNames"> <list>
<value>any Interceptor</value>
</list>
</property>
</bean>
2.自定义TargetSource
实现方法:直接扩展Targetsource接口即可。
Targetsource接口声明:
public interface TargetSource extends TargetClassAware{
Class getTargetClass();
boolean isStatic();
Object getTarget()throws Exception;
void releaseTarget(Object target)throws Exception;
}
各方式解析:
- getTargetClass():返回目标对象类型。
- isStatic():是否要返回同一个目标对象实例。SingletonTargetSource的这个方法肯定是返回true,其他的实现根据情况,一般返回false。
- getTarget():要返回哪个目标对象实例。
- releaserarget():释放对象。当具体调用过程结束时,如果 isStatic()为false,则会调用releaserarget()。不过是否需要释放,完全由实现的需要决定的,该方法可以也空着不实现。
以下应用举例:
// 当计数器为奇数的时候,Targetsource将针对当前调用,返回第一个目标对象实例;否则,返回第二个目标对象实例。
public class AlternativeTargetSource implements TargetSource{
private ITask alternativeTaskOne;
private ITask alternativeTaskTwo;
private int counter;
public AlternativeTargetSource(ITask task1,ITask task2){
this.alternativeTaskOne = task1;
this.alternativeTaskTwo = task2;
}
public Object getTarget() throws Exception{
try {
if (counter % 2 == 0){
return alternativeTaskTwo;
}else {
return alternativeTaskOne;
}finally{
counter++;
}
}
}
public Class getTargetClass(){
return ITask.class;
}
public boolean isStatic(){
return false;
}
public void releaseTarget(Object arg0)throws Exception{
// 什么也不做
}
}
}
测试代码:
ITask task1 = new ITask(){
public void execute(TaskExecutionContext ctx){
System.out.println("execute in Task1.");
}
};
ITask task2 = new ITask(){
public void execute(TaskExecutionContext ctx){
System.out.println("execute in Task2.");
}
};
ProxyFactory pf = new ProxyFactory();
TargetSource targetSource = new AlternativeTargetSource(task1, task2);
pf.setTargetSource(targetSource);
Object proxy =pf.getProxy();
((ITask)proxy).execute(null);
((ITask)proxy).execute(null);
((ITask)proxy).execute(null);
((ITask)proxy).execute(null);
((ITask)proxy).execute(null);
// 程序输出,如下所示∶
// execute in Task2.
// execute in Task1.
// execute in Task2.
// execute in Task1.
// execute in Task2.
如果无需依赖SpringIOC容器,也可以考虑基于org.springframework.aop.target包中的几个抽象类来实现。
提问
AOP的实现机制?涉及哪些类?如何实现?
第10 章 SpringAOP二世(待补充)
10.1 @AspectJ形式的Spring AOP
注解方式使用AOP。底层仍然是前文提到的实现。
引入了AspectJ形式的语言,但是底层实现还是Spring的实现。
SpringAOP会根据标注的注解搜索这些Aspect定义类,然后将其织入系统。
下面来看如何使用。
1.@Aspect形式AOP使用示例
首先定义一个Aspect:
@Aspect
public class PerformanceTraceAspect(
private final Log logger = LogFactory.getLog(PerformanceTraceAspect.class);
@Pointcut("executton{public void *.method1()} || executlon(public void *.mthod2())")
public void pointcutName() {}
@Around("pointcutName()")
public Object performanceTrace(ProceedingJoinPoint joinpoint) throws Throwable {
StopWatch watch= new StopWatch();
try {
watch.start();
return joinpoint.proceed();
} finally {
watch.stop();
if(logger.isInfoEnabled()) logger.info("PT in method["
+ joinpoint.getSignature().getName()
+ "]>>>>>"+watch.toString());
}
}
}
假设目标类如下:
public class Foo {
public void method1() {
System.out.println("method1 execution.");
}
public void method2() {
System.out.println("method2 execution.");
}
}
以下示范编程方式注入和自动代理注入
1编程方式织入
我们用ProxyFactory的兄弟,org.springframework.aop.aspectj.annotation.AspectJProxyFactory
注入。
AspectJProxyFactory weaver = new AspectJProxyFactory();
weaver.setProxyTargetClass(true);
weaver.setTarget(new Foo());
weaver.addAspect(PerformanceTraceAspect.class);
Object proxy= weaver.getProxy();
((Foo)proxy).method1();
((Foo)proxy).method2();
2.通过自动代理织入
针对@AspectU风格的AOP,Spring AOP专门提供了一个AutoProxyCreator
实现类进行自动代理,以免去过多编码和配置的工作。
在xml中注入:
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
<bean id="target" class="org.darrenstudio.books.unveilspring.aop.aspectj.Foo"> </bean>
<bean id="performanceAspect" class="org.darrenstudio.books.unveispring.aop.aspectj.PerformanceTraceAspect"/>
现在,AnnotationAwareAspectJAutoProxyCreator
会自动搜集IoC容器中注册的Aspect,并应用到Pointcut定义的各个目标对象上。
2.@AspectJ形式的Pointcut
1.@AspectJ形式Pointcut的声明方式
通常使用org.aspectj.lang.annotation.Pointcut
注解进行声明。声明示例:
@Aspect
public class YourAspect {
@Pointcut("execution(void method1())") //pointcut_expression
public void method1Exec(){} //pointcut_sigmatu
@Pointcut("method1Exec()") //pointcut_expression 这里是直接引用前面的。定义为public,在其他地方也可以引用
private void stillMehtod1Execution(){} //pointcut_sigmatur
@Pointcut("execution(void method2()")
private void method2Exec(){}
@pointcut("execution(void method1())IIexecution(void method2())") public void bothMethodExec()()
}
2.@AspectJ形式Pointcut表达式的标志符
Spring AOP只支持方法级别的Joinpoint,所以Pointcut表达式有一定限制,只能用到AspectJ的少数标识符:
execution
方法的返回类型、方法名以及参数部分的匹配模式是必须指定的,其他部分的匹配模式可以省略。
加入我们的类定义为:
public class Foo{
public void doSomething(String arg){
// ...
}
}
匹配Foo的dosomething的方法执行,表达式为∶
execution(public void Foo.doSomething(String))
// 可简化为:
execution(void doSomething(String))
支持通配符:
- *:execution(* *(String)),所有入参为String类型的方法。execution(* *(*)),所有只有一个参数的方法,参数类型为任何类型。
- …:
- 表示多个层次:execution(void cn.spring21..*.doSomething(*))
- 表示0到多个参数:execution(void *.doSomething(..))
within
只支持类型级别。匹配该类型下所有pointcut。
within(cn.spring21.aop.target.*)
//匹配cn.spring21.aop.target包下所有类型内部的方法级别的Joinpoint
this和target
在SpringAOP中的语义如下(和原生的Aspect所用语言不太一样):
- this:目标对象的代理对象
- target:目标对象
通常,this和target标志符都是在Pointcut表达式中与其他标志符结合使用:
execution(void cn.spring21.*.dosomething(*)) && this(TargetFoo)
args
捕捉拥有指定参数类型、指定参数数量的方法级Joinpoint,而不管该方法在什么类型中被声明。
例如:
// 声明:
args(cn.spring21.unveilspring.domain.User)
// 下面两个都会被匹配到:
public class Foo{
public boolean login(User user)(...);
}
public class Bar{
public boolean isLogin(User user)(..);
}
注意:它是在运行期间动态检查参数的。即使声明为Object,也会被识别。对应的,类似于execution(* *(User))的表达式,是静态的。
@within
用于声明某个类型注解,匹配被这个类型注解标记的类型下面的所有Joinpoint。
// 注解
@Retention(RetentionPolicy.RUNTIME)
@Target((ElementType.METHOD,ElementType.TYPE))
public @interface AnyUoinpontAnnotation{
}
// 类声明
@AnyJoinpontAnnotation
public class Foo{
public void method1(){...}
public void method2(){...}
}
// 匹配method1(),method2()
@within(AnyJoinpontAnnotation)
@target
@target和@within区别不大。前者是动态匹配,后者是静态匹配。
@args
传入的参数被@args声明的注解标记,则当前Joinpoint被匹配。动态检查。
@annotation
某个方法被@annotation声明的注解标记,则当前Joinpoint被匹配。
// 声明
@annotation(org.springframework.transaction.annotation.Transactional)
// 匹配
public class MockService{
@Transactional
public void service(){
}
}
所有以@开头的标志符,都只能指定注解类型参数。
3.@AspectJ形式的Pointcut在Spring AOP中的真实面目
@AspectJ形式声明的所有Pointcut表达式,都会被Spring AOP转化为具体的Pointcut对象。具体如下图AspectJExpressionPointcut
,是面向AspectJ的实现:
ExpressionPointcut
和AbstractExpressionPointcut
是扩展性需要,如果还有AspectJ的Pointcut描述语言之外的形式,可基于它们来集成。- 获取
Pointcut
表达式并生成AspectJExpressionPointcut
对象:- 获取定义:
AspectJProxyFactory
或AnnotationAwareAspectJAutoProxyCcreator
通过反射获取Aspect中的@Pointcut
定义。 - 生成
Pointcut
对象:Spring AOP框架内部构造一个对应的AspectJExpressionPointcut
对象实例。其内部持有刚才通过反射获得的Pointcut表达式。
- 获取定义:
- 使用
Pointcut
对象来进行匹配:AspectJExpressionPointcut
通过ClassFilter
和MethodMatcher
进行具体Joinpoint
的匹配即可。具体是委托给Aspect类库来做:- 生成匹配对象:委托
AspectJ
类库中的PointcutParser
解析它持有的Pointcut表达式,解析完成之后会返回一个PointcutExpression
对象 - 使用匹配对象进行匹配:直接委托
PointcutExpression
对象的对应方法来判断是否匹配即可
- 生成匹配对象:委托
3.@AspectJ形式的 Advice
即使用@Aspect标注的Aspect定义类中的普通方法,注解包括:
- @Before:标注方法
- @AfterReturning:标注方法
- @AfterThrowing:标注方法
- @After:标注方法
- @Around:标注方法
- @DeclareParents:标注Introduction类型的Advice,但该注解对应标注对象的域(Field),而不是方法(Method)。
用法示例:
@Aspect
public class MockAspect {
@Pointcut("execution(* destroy(..)")
public void destroy(){}
@Before("execution(public void *.methodName(String))")
public void setUpResourceFolder(){
//...
}
@After("destroy()")
public void cleanUpResourcesIfNecessary() {
//...
}
}
1.Before Advice
声明方式参照上述代码即可。
这里重点说下,如何在Advice定义中访问Joimpoint处的方法参数,有两种方式:
通过
org.aspectj.lang.JoinPoint
:将方法的第一个参数声明为JointPoint类型即可。之后可以从JointPoint获取各种信息,如如,getThis()获得当前代理对象,getTarget()取得当前目标对象,getSignature()取得当前Joinpoint处的方法签名。@Before(.....) public void setupResourcesBefore(JoinPoint joinpoint)throws Throwable { joinpoint.getArgs(); }
通过args标志符绑定:args前面讲到可以用来限定Pointcut具体的生效对象类型。这里,当args是某个参数名称的时候,可以把这个参数名称对应的参数值绑定到对Advice方法的调用。
@Before(value="execution(boolean *.execute(String.) && args(taskName)") public void setupResourcesBefore(String taskName) throws Throwable { // 访问'taskName'参数... args // 指定的参数名称必须与Advice定义所在方法的参数名称相同,这里都是taskName } // 通过方法名引用,一样的 @Pointcut("execution(boolean *.execute(String..)ss args(taskName)") private void resourcesetupJoinpoints(String taskName) {} @Before(value="resourceSetupJoinpoints(taskName)") public void setupResourcesBefore(String taskName)throws Tmhrowable {} // 也可同时使用args和JoinPoint @Before(value="execution(boolean *.execute(String,..))&& args(taskName)") public void setupResourcesBefore(JoinPoint joinpoint,String taskName) throws Tmrowable { // access'taskName'argument }
当目标对象上Joinpoint处的方法标注了org.springframework.transaction.annotation.Transactional,我们可以通过这种方式取得其事务设置的详细信息。
2.After Throwing Advice
使用@AfterThrowing标注。@AfterThrowing有一个独特的属性,即throwing,通过它,我们可以限定Advice定义方法的参数名,并在方法调用的时候,将相应的异常绑定到具体方法参数上:
@Aspect
public class ExceptionBarrierAspect{
private JavaMailSender mailSender;
private String[] receiptions;
@AfterThrowing(pointcut="execution(boolean *.execute(String,..)", throwlng="e")
public vold afterThrowing(RuntimeException e) {
final String exceptionMessage = ExceptionUtils.getPullBtackrrace(e);
getMailSender().send(new MimeMessagePreparator() {
public vold prepare(MimeMeasage message) throws Exception {
MmeMessageHelper helper = new MimeMessageHelper(meBsage);
helper.setSubject("...");
helper.setTo(getReceiptions());
helper.setText(exceptionMeasage);
}
});
}
}
3.After Returning Advice
略。
4.After(Finally)Advice
略。
5.Around Advice
对于AroundAdvice的方法定义来说,它的第一个参数必须是org.aspectj.lang.ProceedingJoinPoint
类型(org.aspectj.1ang.JoinPoint的子类),且必须指定。通常情况下,需要通过ProceedingJoinPoint的proceed()
方法继续调用链的执行:
@Aspect
public class PerformanceTraceAspect {
private final Log logger =LogFactory.getLog(PerformanceTraceAspect.class);
@Around("execution(boolean *.execute(String,..))")
public Object performanceTrace(ProceedingJoinPoint jofnyolnt) throws mhrowable {
Stopwatch watch = new StopWatch();
try{
watch.start();
return joinpolnt.proceed();
} finally {
watch.stop();
if (logger.isInfoEnabled()) {
logger.info(watch.toString());
}
}
}
}
6.Introduction
用于标注字段,而不是方法。Spring中,Introduction的实现是通过将需要添加的新的行为逻辑,以新的接口定义增加到目标对象上。例如:
将ICounter
的行为逻辑加到ITask
类型的目标实现类上。假设ITask
的实现类是MockTask
,而ICounter
的实现类是CounterImpl
,我们通过如下的Aspect
声明将ICounter
的行为introduce
附加到ITask
之上:
@Aspect
public class IntroductionAspect {
@DeclareParents(
value="...MockTask",
defaultImpl=CounterImpl.class
)
public ICounter counter;
}
注意到:
@DeclareParents
所归属的域定义类型为ICounter
- 通过value属性,可指定将要应用到的目标对象。可批量指定,例如:
value="cn.spring21.unvellapring.service.*"
- 通过defaultImpl属性,可指定新增加的接口定义的实现类。
注入完成后,不同的目标对象对应着不同的ICounter实例。
Introduction属于per-instance类型的Advice,所以一般目标对象的scope通常情况下应该设置为prototype。
4.@AspectJ中的Aspect 更多话题
1.Advice的执行顺序
多个Advice的Pointcut定义匹配同一个Joinpoint时,优先级如何确定:
在同一个Aspect内:按照声明顺序,在前的、拥有更高的优先级。对于BeforeAdvice,优先级越高越靠前执行。对于AfterReturngingAdvice,优先级越高越靠后执行。
在不同的Aspect内:需要让相应的Aspect定义实现
org.springframework.core.Ordered
接口即可,否则Advice的执行顺序不确定。ordered.getOrder()的返回值越小,优先级越高。当然,如果不是自动代理实现,而是手动代码实现,则按照编码的方式来使用Aspect:
AspectJProxyFactory weaver = new AspectJProxyFactory(); weaver.setProx TargetClass(true); weaver.setTarget(new MockTask()); weaver.addAspect(new AnotherAspect()); weaver.addAspect(new MultiAdvicesAspect(); MockTask task =(MockTask)weaver.getProxy (); task.execute(null);
2.Aspect的实例化模式
对于注册到容器的各个Aspect,它们默认的实例化模式是singleton。
Spring2.0之后的AOP只支持默认的singleton、perthis和pertarget三种实例化模式。不想用默认的,可以指定:
@Aspect("perthis(execution(boolean *.execute(string,..)))")
public class MultiAdvicesAspect{
@Pointcut("execution(boolean *.execute(String,.))")
public void taskExecution(){}
}
}
- singleton:单例模式
- perthis:为相应的代理对象实例化各自的Aspect实例。
- pertarget:为匹配的单独的目标对象实例化相应Aspect实例。
使用perthis或者pertarget实例化模式后,这些Aspect注册到容器时,不能为其bean定义指定singleton的scope,应该是prototype,否则会报错。
10.2 基于Schema的 AOP
Spring框架从1.x版本升级到2.x版本之后,提倡的容器配置方式从基于DTD的XML转向了基于Schema的XML。
新的基于Schema的配置方式为Spring的AOP功能专门提供了独有的命名空间。
使用示例:
<?xml version="1.0" encoding="UTP-8"?>
<!-- 注意aop的引用声明 -->
<beans xmlns="http://www.springframework.org/schema/beans" → Xmlns:xsi="http://ww.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-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<aop:config proxy-target-class="false">
<aop:po1ntcut/>
<aop:advisor/>
<aop:aspect></aop:aspect>
</aop:config>
</beans>
后续具体略。
10.3小结
目前为止,总共有三种Spring AOP的使用方式:
- Spring AOP1.x版本的基于接口定义的Advice声明方式:各种类型的Advice定义需要实现特定的接口,Advice的管理可以通过loC容器或者直接编程来进行。
- Spring 2.0发布之后的@AspectJ形式:只需要使用注解标注POJO中的Advice定义方法即可,不用实现规定的接口来实现各种Advice类型。
- Spring 2.0发布之后 基于Schema的AOP:相应注解表达的各种信息,转移到了XSD形式的容器配置文件。是介于上述两者间的一个妥协,不需要Java5。基于Schema的AOP允许我们使用POJO来声明相应的Aspect和Advice定义,但是不强求使用Java5以上版本的Java虚拟机。
@AspectJ形式最终的底层实现还是基于第一代SpringAOP的各种理念和实体。虽然第一代Spring AOP不像2x之后能够直接通过POJO声明Aspect和Advice,但是它的Advice实现依然拥有很强的表现力。
提问
注解形式AOP的实现机制?
多个Advice匹配到同一个方法,它们执行的优先级如何确定?同一个Aspect内按声明顺序。不同Apsect,按Ordered接口。
Aspect是单例的吗?
第11章 AOP应用案例
本周讲AOP的最佳实践。
- 异常处理
- 安全检查
- 缓存
11.1 异常处理
AOP用于同一异常处理。
Java异常谱系如下:
11.2 安全检查
1.过滤器
javax.servlet.Filter
是Servlet规范为我们提供的一种AOP支持。可以用来进行资源访问控制等。
2.拦截器
类似的,通过AOP,我们可以为任何类型的应用添加相应的安全支持。代码示例:
@Aspect
public class SecurityAspect {
// 注意到 ProceedingJoinPoint pjp
@Around("...")
public Object doCheck(ProceedingJoinPoint pjp) throws Throwable {
if (isIllegalRequest(pjp)) {
throw new SecurityCheckingException("necessary information");
}
return pjp.proceed();
}
}
已经有现成的安全框架。Spring Security(之前叫Acegi框架)在Spring基础上,提供了完备的系统认证、授权、访问控制等安全检查功能。
11.3 缓存
代码示例:
@OAspect
public class CachingAspect {
private static Map cache = new LRUMap(5);
@Around("...")
public Object doCache(ProceedingJoinPoint pjp, Object key) throws Throwable {
if(cache.containsKey(key)) {
return cache.get(key);
} else {
Object retValue = pjp.proceed();
cache.put(key,retValue);
return retValue;
}
}
已经有现成的缓存框架,如EHCache等。
此外,Spring 3.1引入了缓存技术,它本质上不是一个具体的缓存实现方案(如EHCache),而是一个对缓存使用的抽象,可以集成各种缓存技术。通过在既有代码中加入少量它定义的各种 annotation(如@cacheable),即能够达到缓存方法的返回对象的效果。
除此之外,事务也是AOP的典型运用。后续讲解。
提问
Java异常谱系?
拦截器和过滤器有什么区别?
AOP有哪些典型应用场景?
第12章 Spring AOP之扩展篇
本章说的就是,类内方法A调用方法B,方法B的AOP失效的问题。和事务失效一样的问题。
12.1 现象
类内方法A调用方法B,方法B的AOP失效。
12.2 原因
当method调用method2时,它调用的是TargetObject上的method2,而不是ProxyObject上的method2。
针对method2的横切逻辑,只织入到了ProxyObject上的method2方法中,所以,在method中所调用的method2没有能够被成功拦截。
12.3 解决方案
当目标对象依赖于自身时,我们可以把目标对象的代理对象公开给它,只要让目标对象能调用到自身代理对象上的相应方法即可。
Spring AOP提供了AopContext来公开当前目标对象的代理对象,我们只要*在目标对象中使用AopContext.currentProxy()*就可以取得当前目标对象所对应的代理对象,然后显示指定即可。
public class NestableInvocationBO{
public void method1() {
((NestableInvocationBo)AopContext.currentProxy()).method2(); // 显示用代理对象指定方法即可
System.out.println("methodl executed!");
}
public void method2() {
System.out.println("method2 executed!");
}
}
我们在生成目标对象的代理对象时,需要将ProxyConfig或者它的相应子类的exposeProx属性设置为true。
上面实现方案不太优雅,其他解决思路:
- 使用IOC注入一个具体字段。
- 使用统一的工具类来封装上述操作:在Util类中直接声明一个getProxy(),将return AopContext.currentProxy()类似逻辑添加到这个方法中,分离了目标对象与Spring API的直接耦合。
- 批量替换方法:为类似的目标对象声明统一的接口定义,然后通过BeanPostProcessor处理这些接口实现类,将实现类的某个取得当前对象的代理对象的方法逻辑覆盖掉。这与方法替换所使用的原理一样,只不过可以借助Spring的IoC容器进行批量处理而已。
提问
类内方法A调用方法B,方法B的AOP失效,原因是什么?如何解决?
没走代理。在目标对象中使用AopContext.currentProxy()
显示指定代理方法即可。
第4部分 使用Spring 访问数据
对于数据访问部分,Spring提供了三个部分的内容:
- 一套统一的数据访问异常层次体系:定义了一套标准的数据访问异常体系,和具体的数据访问技术解耦。
- 一套JDBC的API的最佳实践:主要解决了JDBC的API的两个问题:
- SQLException不是彻彻底底的标准,还需要结合具体的RDBMS厂商才能判定具体的异常。
- JDBC API太贴近底层,太繁琐。容易导致连接未释放等问题。
- 一套统一的ORM方案集成方式:大部分的ORM API(对象关系映射)在使用上与JDBC API非常相似,所以,Spring也以与JDBC API的最佳实践同样的方式,集成了现有的各种ORM方案。同时,也将这些ORM特定的异常纳入了它的统一的异常层次体系。
第13章 统一的数据访问异常层次体系
13.1 DAO模式的背景
DAO模式:即Data Access Object,数据访问对象。由JavaEE提出。用于解耦数据的访问和存储。无论是用csv文件还是用RDBMS,都用同一套访问接口。
接口示例:
// 根据不同的存储方式,实现以下接口即可。
public interface ICustomerDao{
Customer findCustomerByPK(String customerId);
void updateCustomerStatus(Customer customer);
}
13.2 遇到的问题
问题:DAO里面的受检异常怎么处理?由于要让客户端知道出错了,所以要抛出来。那么Dao就增加了异常处理:
// 加了异常抛出代码
public interface ICustomerDao{
Customer findCustomerByPK(String customerId) throws SQLException;
void updateCustomerStatus(Customer customer);
// 问题:不同的数据存储方式,需要处理不同的异常。需要不断新增异常:
Customer findCustomerByPK(String customerId) throws SQLException, NanmingException;
}
问题的关键在于,各种不同的数据存储,可能抛出不同的异常。那么只能不断地更改、增加方法签名上抛出的异常,这就没法实现统一的数据访问接口了。
13.3 如何解决
分析下现在的情况:
- 不能不让客户端知道:所以DAO层不能自己处理异常,一定要让客户端知道。
- 又不能让客户端去具体处理:因为抛出的是受检异常的话,要由客户端根据不同的厂商去处理,耦合到一起了。即使客户端处理了,最好的方式也只能是不处理。
综上,最终的方案是:
- 用统一的一套非受检异常标准体系,将所有的异常封装为标准非受检异常抛出。这样一来,客户端即不用具体处理异常,又能感知到异常。
- 具体各个厂商的异常到标准非受检异常的映射封装,由DAO层完成。具体就是由Spring来完成。
13.4 Spring具体的实现方案
Spring框架中统一的异常层次体系所涉及的大部分异常类型都定义在org.springframework.dao
包中。具体如下图:
下面来看下各个异常及职责:
CleanupPailureDataAcceseBxception
:已经成功完成相应的数据访问操作,要对使用的资源进行清理却失败的时候,将抛出该异常。比如,使用JDBC进行数据访问的时候,查询或者更新数据操作完成之后,需要关闭相应的数据库连接,如果在关闭连接的过程中出现SQLException,那么数据库连接没有被释放,导致资源清理失败。DataAccessResourceFailureException
:无法访问相应的数据资源。最常见的场景就是数据库服务器挂掉,此处JDBC抛出其子类即org.springframework.jdbc.CannotGetJdbcConnectionException
。DatasourceLookupFailureException
:查找DataSource失败。一般是JNDI(Java Naming and Directory Interface,Java命名和目录接口)服务上,或者其他位置上。ConcurrencyFailureException
:并发进行数据访问操作失败。比如无法取得相应的数据库锁,或者乐观锁更新冲突等。InvalidDataAccesaApiUsageBxception
:以错误的方式,使用了特定的数据访问API。比如,使用Spring的Jabcremplate的getForObject()方法进行查询操作,却返回多行结果。InvalidDataAcceseResourceUsageExceptio
:以错误的方式访问数据资源。比如传入错误的SQL。DataRetrievalFailureException
:获取预期的数据失败。比如,已知某顾客存在,根据该顾客号获取顾客信息失败。PermissionDeniedDataAccessException
:用户没有权限访问数据。DataIntegrityViolationException
:数据一致性冲突异常。如数据库中已经存在主键为1的记录,又尝试插入同样主键记录。某些业务场景下,可以忽略DataIntegrityviolationException,而将插入操作改为更新操作。UncategorizedDataAccessException
:其他无法详细分类的数据访问异常。这个异常是abstract的,用来支持扩展。
小节
略。参照问题即可。
提问
为什么需要Spring统一数据访问异常体系?解决了什么问题?异常不能不让客户端知道,又不能作为受检异常让客户端去具体处理。综上,只能全部封装分类为标准的非受检异常抛出,同时根据不同的RDBMS实现厂商,将各厂商的异常统一处理归类到标准非受检异常,再抛出给客户端,这个工作就由Spring来完成。
第14章 JDBC API的最佳实践
Spring提供了两种使用JDBC API的最佳实践:
- 基于Template的JDBC使用方式
- 基于操作对象的JDBC使用方式
14.1 基于Template的JDBC使用方式
1.JDBC的问题
JDBC:Java平台访问关系数据库的标准API。
JDBC的缺点:
- API太贴近底层,不好用:对开发人员不友好,要写一大堆重复代码,各种连接、释放连接等。也导致容易经常犯错:如Statement使用完后不及时关闭。创建多个ResultSet或者Statement,最后却只清理了最外层的,而忽视了里层的等等。
- 异常类型和具体厂商耦合,分析异常很麻烦:SQLException没有采用将具体的异常情况子类化,以进一步抽象不同的数据访问异常情况,而是采用ErorCode的方式来区分数据访问过程中所出现的不同异常情况。JDBC规范却把ErorCode的规范制定留给了各个数据库提供商,这导致不同提供商提供的数据库对应不同的ErorCode。进而应用程序在捕获SQLException之后,还要先看看当前使用的是什么数据库,然后再从SQLException中通过getErrorCode()取得相应ErorCode,并与数据库提供商提供的ErorCode列表进行对比,最终才能搞清楚到底哪里出了问题。
2.JdbcTemplate的诞生
JdbcTemplate:为解决JDBC难用的问题,Spring提供了org.springframework.jdbc.core.JdbcTemplate
。之后所有Spring提供的JDBC API最佳实践都基于它来实现。
主要解决了两个问题:
- 以统一的格式和规范来使用JDBC API:封装所有基于JDBC的数据访问代码。
- 统一转译原始
JDBC SQLException
所提供的异常信息:简化了客户端代码对数据访问异常的处理。
JdbcTemplate通过模板模式实现,所以我们先来看下模板模式简介。
1.模板模式
模板设计模式:如果多个类中存在某些相似的算法逻辑或者行为逻辑,可以将这些相似的逻辑提取到模板方法类中实现,然后让相应的子类根据需要实现某些自定义的逻辑。
2.JdbcTemplate的演化
数据库的访问都有以下步骤:
- 获取数据库连接:con = getDatasource().getConnection();
- 根据连接获取语句statement:stmt =con.createsStatement();
- 传入SQL获取结果:ResultSet rs = stmt.executeUpdate(sql);
- 关闭Statement:stmt.close(); stmt = null;
- 处理数据库访问异常:catch(SQLException e) {}
- 关闭数据库连接:finally {con.close()}
使用模板模式实现JdbcTemplate:
public abstract class JdbcTemplate {
public final Object execute(String sql) {
Connection con= null;
Statement stmt = null;
} try {
con= getConnection();
stmt = con.createStatement();
Object retValue = executeWithStatement(stmt,sql); // 注意这里,抽象方法实现
return retValue;
} catch (SQLException e) {
DataAccessException ex = translateSQLException(e);
throw ex;
} finally {
closeStatement(stmt);
releaseConnection(con);
protected abstract Object executewithStatement(Statement stmt, String sql);
// 其他方法定义...
}
}
可以看到,虽然使用模板模式简化了操作。但是,execute是一个抽象方法,每次还得实现新的子类。
所以我们改进一下,用CallBack接口封装具体的操作传入即可。
// 实现逻辑接口封装
public interface StatementCallback {
Object doWithStatement(Statement stmt);
}
// 模板模式 + callback接口封装逻辑
public class JdbcTemplate {
public final Object execute(StatementCallback callback) { // 这里改为接口传入执行逻辑。取消了抽象方法的实现方式
Connection con= null;
Statement stmt =null;
} try {
con= getConnection();
stmt= con.createStatement();
Object retValue = callback.doWithStatement(stmt);
return retValue;
} catch(SQLException e) {
DataAccessException ex = translateSQLException(e);
throw ex;
} finally {
closeStatement(stmt);
releaseConnection(con);
}
// 其他方法定义
}
// 使用示例
JdbcTemplate jdbcTemplate = ...;
final String sql="update...";
StatementCallback callback = new StatementCallback() {
public Obejct doWithStatement(Statement stmt) {
return new Integer(stmt.executeUpdate(sql));
};
jdbcTemplate.execute(callback);
}
Spring的具体实现如下图所示:
org.springframework.jdbc.core.Jdbcoperations
接口定义界定了Jdbcremplate可以使用的所有JDBC操作集合,具体可看源码。
org.springframework.jdbc.support.JdbcAccessor
是它的父类,给Jdbcremplate提供了以下属性:
- Datasource:即
javax.sql.DataSource
。JDBC 2.0引入,用于替代基于java.sql.DriverManager
的数据库连接创建方式。DataSource可看做是作JDBC的连接工厂,可以引入对数据库连接缓冲池以及分布式事务的支持。Spring数据访问层对数据库资源的访问,全部建立在javax.sql.Datasource标准接口之上。 - SQLExceptionTranslator:接口,封装了SQLException的转译工作。
JdbcTemplate中各种模板方法可简单分为4组,根据自由度大小区分:
- 面向Connection的模板方法:自由度太大,除了处理历史遗留问题,一般很少用。通过ConnectionCallback回调接口公开的java.sql.Connection进行数据访问。
- 面向Statement的模板方法:主要处理基于静态的SQL的数据访问请求。通过org.springframework.jdbc.core.statementcallback回调接口对外公开java.sql.Statement进行数据访问。
- 面向Preparedstatement的模板方法:主要处理用包含查询参数的SQL请求,避免SQL注入。通过org.springframework.jdbc.core.PreparedstatementCreator回调接口公开Connection以允许Preparedstatement的创建。
- 面向callablestatement的模板方法:主要用于数据库存储过程的访问。通过org.springframework.jdbc.core.CallableStatementcreator公开相应的Connection以便创建用于调用存储过程的CallableStatement。
源码示例:
// JavaTemplate面向statemenE的核心模板方法定义代码
public Object execute(StatementCallback action) throws DataAccessException{
Assert.notNull(action,"Callback object must not be null");
Connection con = DataSourceUtils.getConnection(getDatasource());
Statement stmt =null;
try {
Connection conToUse = con;
if(this.nativeJdbcExtractor!= null && this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
stmt= conToUse.createStatement();
applyStatementSettings (stmt);
Statement stmtToUse= stmt;
if(this.nativeJdbcExtractor!= null) {
stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
Object result = action.doInStatement(stmtToUse);
handlewarnings(stmt.getwarnings());
return result;
} catch(SQLException ex) {
//Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet. JdbcUtils.closeStatement(stmt); stmt =null;
DatasSourceUtils.releaseConnection(con,getDataSource());
con = null;
throw getExceptionTranslator().translate("StatementCallback",getSql(action), ex);
} finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con,getDataSource());
}
3.使用DataSourceUtils进行Connection的管理
JdbcTemplate取连接时,不是直接通过DataSource.getConnection()方法,而是通过了DataSourceUtils工具类。
// 直接取
Connection con= dataSource.getConnection();
// 通过工具类来取
Connection con = DataSourceUtils.getConnection (getDataSource());
该工具类的核心作用是:DatasourceUtils会将取得的Connection绑定到当前线程,以便在使用Spring提供的统一事务抽象层进行事务管理的时候使用。
4.使用NativeJdbcExtractor来获取原始Connection
Spring返回的是标准代理对象:出于事务控制等考虑,通过JavaEE等DataSource实现中返回的Connection和Statement是代理对象,所以使用的时候也是通过java.sql.Connection接口的方式使用,是标准的方法。
有时候会需要原始对象:如果我们要用数据库的特色功能,就需要返回数据库驱动程序提供的原始Cconnection实现类(例如,oracle.jdbc.OracleCconnection)。
如何返回原始对象:
JdbcTemplate如何区分原始对象的代理对象:JdbcTemplate内部定义了一个NativeJdbcExtractor
类型的字段(即驱动程序提供的具体实现类),然后在使用具体的Connection或者statement之前,会先检查该字段是否为空,不为空就引用它。同时也提供的设置该字段的方法setNativeJdbcExtractor(NativeJdbcExtractor)
。
class JdbcTemplate {
NativeJdbcExtractor nativeJdbcExtractor;
// ...
if(this.nativeJdbcExtractor!= null) {
//Extract native JDBC Connection castable to OracleConnection or the like.
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
// ...
if(this.nativeJdbcExtractor!= null) {
stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
}
// ...
public void setNativeJdbcExtractor(NativeJdbcExtractor) {
//...
}
}
常见的数据源(C3P0这种,本质上就是DataSource)以及对应的NativeJdbcExtractor实现类:
- Commons DBCP:CommonsDbcpNativeJdbcExtractor
- C3P0:C3PONativeJdbcExtractor。
- Weblogic:webLogicNativeJdbcExtractor。
- WebSphere:webSphereNativeJdbcExtractor。
更多实现类参见org.springframework.jdbc.support.nativejdbc包。
5.控制JabcTemplate的行为
查询参数设置:JdbcTemplate在通过statement或reparedStatement等操作数据前,会先调用代码查询设置的参数,如最大返回行数、超时时间等:
applyStatementSettings(stmt);
// 或者
applyStatementSettings(ps);
// 或者
applyStatementSettings(cs);
源码示例:
class JdbcTemplate {
int FetchSize; // 每次最多行数
protected void appilysStatementsettings(Statement stmt) throws sQLException {
int fetchSize = getFetchSize();
if (fetchsize>0) {
stmt.setFetchSize(fetchSize);
}
int maxRows = getMaxRows();
if (maxRows >0) {
stmt.setMaxRows(maxROwS);
}
DataSourceUtils.applyTimeout(stmt,getDatasource(),getQueryTimeout());
}
}
// 使用示例
dbcTemplate jt = new JdbeTemplate(...);
jt.setFetchSize(1000);
//使用jt进行数据访问
6.SQLException到DataAccessException体系的转译
SQLException是JDBC的异常体系,DataAccessException是Spring封装的统一异常体系。
转译工作由org.springframework.jdbc.support.SQLExceptionTranslator
接口来完成。如下图所示:
主要的实现类:
- SQLErrorCodeSQLExceptionTranslator:基于SQLException返回的ErrorCode进行转译。ErrorCode由各数据库提供商提供。该方式用的最多。
- SQLExceptionSubclassTranslator: Spring 2.5新增,将JDBC4版本(JDK6发布)中新定义的异常体系转化到Spring的数据访问异常体系。
- SQLStateSQLExceptionTranslator:根据SQLException.getSQLState()所返回的信息进行异常转译,各数据库厂商在执行上存在差异,基于SQLState不是很准确。
也支持自定义的异常转译,两种方式:
- 扩展SQLErrorCodeSQLExceptionTranslator,增加新子类,覆写它的customTranslate()方法。
- sql-error-codes.xml自定义配置:在XML注入具体的bean即可。
详细实现略。
3.JdbcTemplate和它的扩充
最初,Spring框架只提供了JdbeTemplate一个实现。但随着Java版本升级,并且考虑到使用中的便利性等问题,Spring在新发布的版本中又为JdbcTemplate添加了两位兄弟:
- org.springframework.jabc.core.simple
.SimpleJdbcTemplate
:主要面向Java 5提供的一些便利 - org.springframework.jdbc.core.namedparam.
NamedParameterJdbeTemplate
:可以在SQL中使用名称代替原先使用的?占位符。
1.JdbcTemplate用法示例:
初始化JdbcTemplate:通过构造方法传入它所使用的Datasource(如C3P0等)即可。
基于JdbcTemplate访问数据(加入了查询回调):例如::
long interval= jdjdbcTemplate.queryForLong("select· count(customerId)from customer"); final List customerList = new ArrayList(); jdbcTemplate.query("select * from customer", new RowCallbacklandler() { public void processRow(ResultSet rs) throws SQLException { Customer customer = new Customer(); customer.setFirsEName(rs.getString(1)); customer.setLastName(rs.getString(2)); customerList.add(customer); } });
2.NamedParameterJdbcTemplate的使用示例:略。
3.SimpleJdbcTemplate的使用示例:略。
4.Spring 中的DataSource
Spring的数据访问框架在数据库资源的管理上全部采用JDBC 2.0标准之后引入的javax.sql.DataSource
接口作为标准。
1.DataSource的种类
根据功能强弱,可以划分为三类:
- 简单的DataSource实现:只实现作为ConnectionFactory角色的基本功能,一般不用于生产。Spring提供了两个简单实现:
- org.springframework.jdbc.dataource.
DriverManagerDatasource
:主要用来替换最原始的DriverManager方式。使用时直接通过代码或者XML注入即可。 - org.springframework.jdibe.datasource.
SingleConnectionDatasource
:每次返回同一个连接,相当于只维护一个singleton的Connection的ConnectionFactory。获取到connection后,如果关闭它,再次获取时,就是抛SQLException。
- org.springframework.jdbc.dataource.
- 拥有连接缓冲池的Datasource实现:会维护一个数据库连接池。最常用的DataSource。
- 客户端获取到的Connection对象,如果通过close()方法关闭,实际上只是被返回给缓冲池,而不是真正的被关闭。
- C3PO就是这类DataSource的实现,也可以指定初始连接数、最小连接数、最大连接数等参数。
- 可以直接编码使用,也可以通过IOC注入使用。
- 支持分布式事务的Datasource实现类:除非应用程序确实需要分布式事务,否则一般不用这种类型。
- 都是javax.sql.XADatasource的实现类,返回javax.sql.XAConnection。
- 由于XAConnection扩展了javax.sql.PooledConnection,所以也支持数据库连接缓冲功能。
2.Datasource的访问方式
- 本地Datasource访问:直接编码实现,或者IOC注入。
- 远程DataSource访问:通过JNDI对其进行访问。
在Spring的IoC容器中,我们可以通过org.springframework.jndi.JndiobjectFactoryBean对这些Datasource进行访问,例如∶
<?xml version="1.0" encoding="UF-8" ?>
<beans xmlns="http://ww.springframework.org/schema/beans"→ xmlns:xsi="http://www.w3.org/2001/XML.SChema-instance"
xmlns:jee="http://www.apringframework.org/chema/jee"━
xsi:schemaLocation="http://www.springframework.org/schema/beans →
http://ww.springframework.org/schema/beans/spring-beans-2.0.xsd→
http://www.apringframework.org/schema/jee →
http://ww.apringframework,org/schema/jee/sapring-jee-2.0.xed">
<jndi:lookup id="datasource"jndi-name="java:env/myDataSource"/>
</beans>
3.自定义Datasource实现
实现新的Datasource:
直接扩展org.springframework.jdbc.datasource.
AbstractDataSource
即可。前面的DriverManagerDataSource就是它的实现类。持有多个DataSource需求:扩展org.springframework.jdbc.datasource.lookup.
AbstractRoutingDatasource
。它是AbstractDataSource
的扩展,可以持有多个DataSouce,然后getConnection时,根据待实现的protected abstract Object determineCurrentLookupKey()
方法判断返回哪个数据源。如下图所示:
为现有DataSource添加新行为:
- org.springframework.jdbc.datasource.
DelegatingDataSource
:自身持有一个其他的Datasource实例作为目标对象,调用getConnection()等方法时,把调用转发(即委派)给这个持有的DataSource。所有,我们只需要实现DelegatingDatasource子类,覆写相应的方法,在转发方法调用之前添加相应的自定义逻辑即可。 - Spring提供的现成的实现类有:
- org.springframework.jdbe.datasource.
UserCredentialaDatasourcendapte
:可为DataSource加入验证信息。 - org.epringframework.jdbe.datasource.
TransactionAwareDatasourceProxy
:从它得的Connection将自动加入Spring的事务管理。要想加入Spring的事务管理,我们需要使用DataSourceUtils类进行Connection的管理。TransactionAwareDatasourceProxy
内部也是用DatasourceUtils进行管理的。 - org.sapringframework.jdbc.datasource.
LazyConnectionDatasourceProxy
:从它取得的Connection对象是一个代理对象,该代理对象可以保证当Connection被使用的时候才会从LazyConnectionDataSourceProxy持有的DataSource目标对象上获取。
- org.springframework.jdbe.datasource.
- org.springframework.jdbc.datasource.
5. JdbcDaoSupport
现在实现一个DAO的,肯定不用像原来那样使用底层的JDBC API来实现了。最起码,我们会使用相应的DataSource提供数据库连接,使用JdbcTemplate进行数据库操作。
Spring直接提供了org.springframework.jdbc.core.support.JdbcDaoSupport
,把Datasource和JacTemplate全部提取到统一的超类中。它可以作为所有基于JDBC进行数据访问的DAO实现类的超类:
public class GenericDao extends JdbcDaoSupport implements IDaoInterface {
public void update(DomainObject obj) {}
getJdbcTemplate().update(..);
}
// ···
// setter和getter方法定义
}
14.2 基于操作对象的JDBC使用方式
Spring除了提供基于Template形式的JDBC使用方式,还对各种数据库操作以面向对象的形式进行建模,为我们使用JDBC进行数据访问提供了另一种视角。
在这种基于操作对象的JDBC使用方式中,查询、更新、调用存储过程等数据访问操作,被抽象为操作对象,这些操作对象统一定义在org.springframework.jdbc.object包下,以org.springframework.jdbc.object.RaibmsOperation作为整个操作对象体系的顶层抽象定义:
总体结构:
- 公共设施:RdbmsOperation抽象类,它提供了所有子类所需要的公共设施,包括当前数据库操作对应的SQL语句的声明,参数列表处理,以及进行底层数据库操作所必需的Jdbcremplate实例等。
- 所有的操作对象最终的数据访问都是通过Jdbcremplate进行:实际上,基于操作对象的JDBC使用方式与基于JdbcTemplate的JDBC使用方式是统一的,只不过对待概念的视角上有所不同而已。
- 各抽象类分支:
- SqlQuery抽象类:查询操作对象。
- SqlUpdate实体类:可以直接使用它进行数据库更新操作。
- SqlCall和StoredProcedure:存储过程对象分支。
详细略。
使用示例:
public class CapitalTitleUpdateablesSqlQuery extends UpdatablesqlQuery {
public CapitalTitleUpdateablesqlQuery(Datasource datasource,String sql) {
super(dataSource,sql);
compile();
}
@override
protected Object updateRow(ResultSet rs,int row,Map context)throws SQLException {
String title = rs.getString("news_title");
rs.updateString("news_title",StringUtils.capitalize(title));
return null;
}
}
// 使用:
DataSource datasource =...;//DBCP或者C3P0或者其他数据源实现
String sql ="select * from fx news";
CapitalTitleUpdateableSqlQuery updatableQuery = new CapitalritleUpdateablesqlQuery (dataSource, sql);
updatableQuery.execute();
小结
JDBC在Java平台上的数据访问领域一直占据重要地位,Spring框架针对JDBC API的各种问题,提供了一套最佳实践,包括基于JdbcTemplate和基于操作对象的实践方式。
要访问数据,JDBC并非唯一的选择。还有其他ORM方案,如Mybatis。下一章来看下。
提问
为什么要有JdbcTemplate?1.原始的JDBC贴近底层,编码冗余难用。2.原始的JDBC异常很难分析,异常类型和厂商耦合到一起,所有异常集成到一个SQLException,具体异常根据ErrorCode判断,而ErrorCode又由各个厂商去判断。
JdbcTemplate如何实现?模板模式 + CallBack逻辑封装。模板模式规定执行路径。再优化抽象方法,改为执行逻辑用CallBack接口从方法参数即可。有DataSource和SQLExceptionTranslator属性,有XXXX类模板方法。
SQLException是什么?JDBC的统一抛出异常,根据errorCode对应具体异常。而errorCode又由各个厂商自行规定,导致了SQLExcetion没能和具体厂商解耦。
DataSource和DraviManager有什么区别?javax.sql.DataSource。JDBC 2.0引入,用于替代基于java.sql.DriverManager的数据库连接创建方式。DataSource可看做是作JDBC的连接工厂,可以引入对数据库连接缓冲池以及分布式事务的支持。Spring数据访问层对数据库资源的访问,全部建立在javax.sql.Datasource标准接口之上。
怎样实现自定义的DataSource?Spring事务是怎么操作DataSource的?
JDBC和MyBatis是什么关系?
JDBC是Java提供的一个操作数据库的API; MyBatis是一个持久层ORM框架,底层是对JDBC的封装。
MyBatis对JDBC操作数据库做了一系列的优化:
(1)mybatis使用已有的连接池管理,避免浪费资源,提高程序可靠性。
(2)mybatis提供插件自动生成DAO层代码,提高编码效率和准确性。
(3)mybatis 提供了一级和二级缓存,提高了程序性能。
(4)mybatis使用动态SQL语句,提高了SQL维护。(此优势是基于XML配置)
(5)mybatis对数据库操作结果进行自动映射
第15章 Spring对各种ORM的集成
Spring对各种ORM的集成主要做了以下工作,其中前两点和JDBC是一样的:
- 统一的资源管理方式:类似JDBC的资源管理,Spring框架统一封装了各种ORM的使用方式以及资源管理方式。
- 统一异常体系的转译:转译各ORM特定的数据访问异常,统一纳入到Spring的异常层次体系,把具体的数据访问技术和异常体系解耦。
- 统一的数据访问事务管理及控制方式:Spring为各种数据访问方式(不只是ORM)提供了统一的事务管理抽象层。便于统一管理和控制各种数据访问方式的特定事务。
在此基础上,可以进一步通过IOC来管理。
Spring支持Hibernate、Mybatis等ORM管理。
15.1 Spring 对 Hibernate的集成(略)
略。
15.2 Spring 对 iBATIS的集成
来看下iBatis的原生用法和Spring集成后的用法,两者的区别。
以下两种用法,最大的区别就是事务的处理。
1.iBatis原生的用法
和JDBC一样,Spring同样是解决iBATIS的资源管理、异常处理等各种方面的问题。
先来看下 iBATIS的原始最佳实践,即不用Spring的情况。
BATIS通常通过com.ibatis.sqlmap.client.SqlMapclient
进行数据访问(Mybatis最常用的API是SqlSessionFactory
)。
1.构建SqlMapclient
先准备好配置文件,然后用SqlMapClientBuilder
来构建即可:
Reader reader= null;
SqlMapClient sqlMap =null;
try {
String resource ="com/ibatis/example/sqlMap-config.xml";
reader= Resources.getResourceAsReader(resource);
sqlMap = SqlMapClientBuilder.buildsqlMapClient(reader);
} catch(IOException e) {
e.printStackTrace();// 不要这样做
}
// sqlMap现在处于可用状态
2.使用SqlMapclient
有三种方式来进行数据访问:
基于SqlMapClient的自动提交事务型简单数据访问:资源问题的处理已经封装,不用考虑。
Map parameters = new HashMap(); parameters.put("parameterName", value); // ... Object result = sqMap.queryorObject("Byatem.getSystemAttrlbute", parameters);
基于SqlMapClient的非自动提交事务型数据访问:事务问题需要处理,看起来已经不清爽了:
try { sqlMap.startTransaction(); sqlMap.update("mappingStatement"); sqlMap.commitTransaction(); } catch(SQLException e) { e.printStackTrace();//不要这样做 } finally { try { sqlMap.endTransaction(); } catch(SQLException e) { e.printStackTrace();//不要这样做 } }
基于SqlMapSession的数据访问:此外,也可以从SqlMapClient中获取SqlMapSession,手动管理数据访问资源以及相关的事务控制和异常处理。性能有一些提高,但是代码还是不清爽:
SqlMapSession session = null; try { session = sqlMap.openSession(); ession.startTransaction(); session.update(""); session.commitTransaction(); } catch (SQLException e) { e.printStackTrace();//不要这样做 } finally { if (session!= null) { try { session.endTransaction(); } catch(SQLException e) { e.printStackTrace();//不要这样做 } session.close(); } }
可以看到,如果都用上面的代码开发,不做统一管理,代码是比较乱的。
2.用Spring集成iBatis
Spring用以下两个接口来实现iBatis的集成(经典的模板 + CallBack方法模式):
- org.springframework.orm.ibatis.
SqlMapClienteTemplate
:基于iBATIS进行数据访问操作的模板方法类。 - org.springframework.orm.ibatis.
SqlMapClientCallback
:回调接口,用于传入具体的数据访问逻辑。
1.SqlMapCllentTemplate的实现
核心是实现SqlMapClientTemplate中的 execute(SqlMapClientCallback)
方法。
execute(SqlMapClientCallback)
方法集资源管理、异常处理以及事务控制于一身。以下是其定义代码:
public Object execute(SqlMapClientCallback action) throws DataAccessException {
Assert.notNull(action,"Callback object must not be null");
Assert.notNull(this.sqlMapClient,"No Sq1MapClient specified");
//We always needs to use a SqlMapSession, as we need to pass a Spring-managed
// Connection(potentially transactional) in.This shouldn't be necessary if
//we run against a TransactionAwareDatasSourceProxy underneath, but unfortunately
// we still need it to make iBATIS batch execution work properly:If iBATIS
//doesn't recognize an existing transaction, it automatically executes the
//batch for every single statement...
SqlMapSession session= this.sqlMapClient.openSession();
if(logger.isDebugEnabled()) {
logger.debug("Opened SqlMapSession ["+ session "]for iBATIS operation");
}
Connection ibatisCon = null;
// Spring处理对事务管理的集成。Spring会根据当前的事务设置决定通过何种方式从指定的datasource中获取相应的Connection供sq1MapSession使用
try {
Connection springCon = null;
DataSource dataSource = getDataSource();
boolean transactionAware = (dataSource instanceof TransactionAwareDatasourceProxy);
// Obtain JDBC Connection to operate on...
try {
ibatisCon = session.getCurrentConnection();
if (ibatisCon == null) {
springCon = (transactionAware ?
dataSource.getConnection() : DataSourceUtils.doGetConnection
(dataSource));
session.setUserConnection(springCon);
if (logger.isDebugEnabled()) {
logger.debug("Obtained JDBC Connection["+ springCon +") for iBATIs operation");
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("Reusing JDBC Connection["+ibatisCon +"] for iBATIs operation");
}
}
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
}
// 模板方法调用Sq1MapclientCallback的回调方法来进行数据访问,如果期间出现SOLException,则通过提供的sOLExceptionTranslator进行异常转译。
// 最后,合适地关闭所使用的数据库连接和Sq1lMapsession。
// Execute given callback...
try {
return action.doInSqlMapClient(session);
} catch (SQLException ex) {
throw getExceptionTranslator().translate("SqlMapclient operation", null,ex);
} finally {
try{
if (springCon != null) {
if (transactionAware) {
springCon.close();
} else {
DataSourceUtils.doReleaseConnection(springCon, datasource);
}
}
} catch (Throwable ex) {
logger.debug("Could not close JDBC Connection", ex);
}
}
// Processing finished-potentially session still to be closed.
} finally {
// Only close SqlMapSession if we know we've actually opened it
// at the present level.
if (ibatisCon == null) {
session.close();
}
}
}
2.SqlMapClientTemplate的使用
1.构建SqlMapClientTemplate
SqlMapClientTemplate底层依赖com.ibatis.sqlmap.client.SqlMapClient,所以要给它提供一个SqlMapClient。
String resource= "com/ibatis/example/sq1Map-config.xml"; Readerreader = Resources.getResourceAsReader(resou SqlMapClient sqlMap =Sq1MapClientBuilder.buildSqlMapClient(reader); SqlMapClientTemplate sqlMapClientTemplate = new SqlMapClientTemplate(sq1Map); //sqlMapClientTemplate准备就绪,可以使用
提供一个SqlMapClient后,默认使用BATIS配置文件内部指定的DataSource。要使用外部的DataSource也可以:
//1.定义DataSource BasicDataSource dataSource = new BasicDatasource(); // ··· //2. 定义SqlMapClient SqlMapClient sqlMap = SqlMapClientBuilder.buildsqlMapclient(reader); //3.创建SqlMapClientTemplate SqIMapClientTemplate sqlMapClientTemplate = new SqlMapClientTemplate(datasource,sq1Map); //sqlMapClientTemplate准备就绪,可以使用
除了上述编码方式,也可以用Ioc注入(配置XML)
2.构建SqlMapclientCallback
实现SqlMapclientCallback接口,覆写public Object doInSqlMapClient(SqlMapExecutor executor)
方法即可。SqlMapExecutor提供了几乎所有的iBatis访问功能,如查询、批量更新等。
实现示例:
protected void batchInsert (final List beans) {
sqlMapClientTemplate.execute(new SqlMapClientCallback(){
public Object doInSqlMapClient(Sq1MapExecutor executor) throws SQLException {
executor.startBatch();
Iterator iter = beans.iterator();
while(iter.hasNext()) {
YourBean bean = (YourBean)iter.next();
executor.insert("Namespace.insertStatement", bean);
}
executor.executeBatch();
return null;
}
});
}
3.用SqlMapClientTemplate进行数据访问操作
所有的SqlMapClientTemplate中定义的数据访问操作方法,都在org.springframework.orm.ibatis.SqlMapClientOperations接口中定义。
使用示例:
SqlMapClientTemplate sqlMapClientTemplate = ...;
Object parameter =...;
// 1.插入数据
sqlMapClientTemplate.insert("insertStatementName",parameter);
3.SqlMapCllentDaoSupport
Spring提供了org.springframework.orm.ibatis.support.SqIMapClientDaoSupport
作为整个DAO层次体系的基类。所有开发人员在使用iBATIS进行DAO开发的时候,直接继承sqlMapclientDaosupport类即可。
15.3 Spring 中对其他ORM方案的集成概述
集成的关注点大同小异,其他ORM的集成支持有:
- JDO:Sun提出来的数据持久化规范
- TopLink
- JPA:JPA(Java Persistence API,Java持久化API)是Sun于JavaEE5之后提出的ORM解决方案的统一标准,具体实现由不同提供商提供,包括Hibemate、Toplink等。就好像当年的JDBC标准一样,只不过,JPA是面向ORM的统一。Spring框架在2.0版本之后提供了对JPA的支持,并且在之后的版本中做进一步的统一和完善。
详细略。
提问
MyBatis和JDBC什么关系?
Spring集成ORM主要解决了什么问题?统一的数据访问方式,统一的异常处理体系,统一的事务控制体系。
Spring如何集成iBatis? SqlMapClientTemplate + SqlMapclientCallback
第16章 Spring数据访问之扩展篇
本章内容:
- 活用模板方法模式及Callback
- 数据访问中的多数据源
- Spring 3.0展望
16.1 活用模板方法模式及Callback
就是经典的模板方法设计模式 + CallBack传入执行逻辑。
类似JDBC Template的实现,以下两个可以用这个经典模式:
- FTPClientTemplate
- HttpClientTemplate
详细略。
16.2 数据访问中的多数据源
1.主权独立”的多数据源
每个数据源对应一个数据库:
配置示例:
<bean id="mainJdbcTemplate" class="org.springframework.jdbc.core.JdbcTremplate">
<property name="dataSource"ref="mainDataSource"/>
</bean>
<bean id="infoJdbcTemplate"class="org.springframework.jdbec.core.JdbecTemplate">
<property name="dataSource"ref="infoDataSource"/>
</bean>
2.合纵连横”的多数据源
统一用一个DataSource来对”联盟”内的多个Datasource的职能进行协调和管理,最终数据访问所需要的资源由”盟主”来决定要哪一个Datasource提供。
使用场景举例:
- 多机热备
- 负载均衡
实现思路:实现一个自定义的Datasource,让该DataSource来管理系统中存在的多个与具体数据库挂钩的数据源,数据访问类只跟这个自定义的DataSource打交道即可。
Spring2.0.1之后,引入了AbstractRoutingDatasource
。使用该类可以实现普遍意义上的多数据源管理功能。要继承该类,通常只需要给出determineCurrentLookupKey()
方法的逻辑即可。可通过重新设置DatasourceLookup来实现我们自己的键查找行为。
16.3 Spring 3.0展望
略。
总结
Spring数据访问层中重用模板方法模式与callback接口相结合的问题处理的理念。
提问
模板方法设计模式 + CallBack 的经典设计模式由哪些运用?
第5部分 事务管理
本部分内容:阐述Spring事务管理抽象层的理念以及相关内容。
第17章 事务引言
1.什么是事务
数据访问操作需要做限制:数据保存了系统状态,为了保证系统始终处于”正确”的状态,需要对数据访问操作进行一些必要的限定。
事务:以可控的方式对数据资源进行访问的一组操作。
事务本身持有4个限定属性,即ACID属性:
- 原子性:事务的所有操作不可分割。要么全部完成,要么全部失败。
- 一致性:事务不破坏数据的一致性。例如,AB两个账户转账前后,他们的存款总和是一致的。
- 隔离性:事务之间相互隔离,互不影响。多个事务访问同一个数据时,相互的影响程度,可以分为4种隔离级别:
- 读未提交:无法避免脏读、不可重复读、幻读。
- 脏读:事务A对同一个数据读取,每次读取到的结果都不一样。原因是事务A读到事务B未提交的数据。
- 不可重复读:事务A对同一个数据读取,每次读取到的结果都不一样。原因是事务A读到事务B已提交的数据。提交完成后,数据的值被事务B改了。
- 幻读:事务A对同一个数据读取,每次读取到的结果都不一样。同样是一个查询在整个事务过程中多次执行后,查询所得的结果集不一样。但是幻读针对的是多笔记录。不管事务B有没有提交,事务A读到的结果都有可能不同。和事务B提交没提交没关系。
- 读已提交:无法避免不可重复读、幻读。
- 可重复读:无法避免幻读。
- 序列化:可避免所有情况。所有事务串行执行。
- 读未提交:无法避免脏读、不可重复读、幻读。
- 持久性:一旦事务提交成功,结果不能再变。
数据库一般是默认读已提交级别(如Oracle),MySQL是默认可重复读级别。
2.处理事务的各参与方
典型的事务处理场景,有以下参与者:
- Resource Manager:简称RM。负责存储管理数据。如数据库服务器、MQ、JMS消息服务器
- Transaction Processing Monitor:简称TPM或者TP Monito。在分布式事务场景中协调包含多个RM的事务处理。如Tomcat。
- Transaction Manager:简称为TM,它可以认为是TP Monitor中的核心模块,直接负责多RM之间事务处理的协调工作。并且提供事务界定(Transaction Demarcation)、事务上下文传播(Transaction Context Propagation)等功能接口。
- Application。以独立形式存在的或者运行于容器中的应用程序,可以认为是事务边界的触发点。如:前端、触发事务的其他后端?
根据中涉及的RM数量,将事务分为两类:
- 全局事务:即分布式事务。多个RM参与。需要引入TPMonitor来协调多个RM之间的事务处理。TPMonitor将采用两阶段提交(Two-Phase Commit)协议来保证整个事务的ACID属性。
- 两阶段提交:最经典的比喻就是结婚,牧师会问新郎新娘是否同意,都同意才生效。实际上2PC性能很差,实际生产中用的比较少。
- 局部事务:当前事务只有一个RM参与。如只对一个数据库进行更新,或者只向一个消息队列中发送消息。此时应用程序直接与RM打交道即可,无需引入TP Monitor。
提问
事务的ACID特性?以及对于I的4个隔离级别区别?MySQL底层实现?
分布式事务常见实现方案?2PC,TCC事务补偿(阿里的框架),本地消息表(FaceBook),RocketMQ(基于可靠消息的最终一致性),最大努力通知。
第18章 群雄逐鹿下的Java事务管理
本章内容:各事务处理场景下,可以通过哪些产品提供的事务处理接口(或者标准的事务处理接口)来进行事务控制。
从局部事务到分布式事务的顺序,介绍在各个场景中,Java平台为我们准备的事务API。
这里我们看下,各个业务场景下,原生的事务管理是怎么做的。
18.1 Java 平台的局部事务支持
Java的局部事务场景中,没有专用的事务API来管理事务。不同的数据访问技术,会提供各自的事务访问API,使用自己的连接来进行事务管理。
1.JDBC API事务举例
Connection connection = null;
boolean rollback = false;
try{
connection = dataSource.getConnection();
connection.getAutoComit(false); // 事务处理代码
//使用JDBC进行数据访问
connection.commit(); // 事务处理代码
} catch (SQLException e) {
e.printStackTrace();//不要这样做
rollback = true;
} finally {
if (connection!= null) {
if (rollback) {
try {
connection.rollback(); // 事务处理代码
} catch(SQLException e) {
e.printStackTrace();//不要这样做
}
} else {
try{
connection.close();
} catch(SQLException e) {
e.printStackTrace();//不要这样做
}
}
}
}
2.JMS事务管理代码示例
boolean rollback = false;
Connection con = null;
Session session = null;
try{
con= cf.createConnection();
session= con.createSession(true, Session.AUTO_ACKNOWLEDGE); // 使用JMSAPI处理响应消息。//注意到true,要求创建一个事务型的javax.jms.Session实例然后就可以根据情况提交或回滚。
sesaion.commit(); // 事务处理代码
} catch (JMSException e) {
e.printStackTrace();//不要这样做
rollback = true;
} finally {
if (con!= null) {
if (rollback) {
try {
session.rollback(); // 事务处理代码
} catch(SQLException e) {
e1.printStackTrace();//不要这样做
}
} else {
try{
con.close();
} catch(SQLException e) {
e.printStackTrace();//不要这样做
}
}
}
}
18.2 Java 平台的分布式事务支持
Java平台上的分布式事务管理,主要通过JTA(JavaTransaction API)或者JCA(Java ConnetorArchitecture)支持。
1.基于JTA的分布式事务管理
JTA是Sun公司提出的标准化分布式事务访问的Java接口规范。不过,JTA规范定义的只是一套Java接口定义,具体的实现留给了相应的提供商去实现,各JavaEE应用服务器需要提供对JTA的支持。通过编码实现,或配置到XML文件均可。
UerTransaction ut = (UserTransaction)ctx.lookup("javax.transaction.UserTransaction");
ut.begin();
详细略。
2.JTA声明性事务管理
由EJB提供。在XML中指定由EJB来管理事务即可。
<enterprise-beans>
<seSsion>
<display-name>Your Enterprise Java Bean</display-name>
<ejb-name>YourBean</ejb-name>
<home>...</home>
<remote>...</remote>
<ejb-class>...YourBean</ejb-class>
<session-type>Stateless</session-type>
</session>
<transaction-type>Container</transaction-type> <!-- 注意这里 -->
</enterprise-beans> <assembly-descriptor> <container-transaction>
<method>
<ejb-name>YourBean</ejb-name>
<method-name>*</method-name>
</method>
<trans-attribute>Required</trang-attribute> <!-- 注意这里 -->
</container-transaction>
</assembly-descriptor>
3.基于JCA 的分布式事务管理
JCA规范主要用来集成遗留的EIS(Enterprise Information System),向它提供接口。
18.3 Java原生事务管理的弊端
Java平台提供的原生事务管理API很丰富,但是有很多弊端:
- 事务控制的API和数据资源耦合:JDBC通过java.sql.connection来控制,Hibemate又用org.hibernate.Session和org.hibernate.ransaction来控制。以上两种方式,事务控制的API都是从数据资源的连接里面获取的。
- 没有统一的事务相关异常体系:理论上来说,事务不可恢复,应该抛出unchecked exception,并有一个统一的父类,方便客户端处理。实际上却是,有受检异常有非受检异常,而且不同的厂商处理的异常又不一样。
- 没有统一的事务管理API:其实对于不同的数据访问,开发人员要做的工作只是规定事务什么时候开始,什么时候结束就好了。但事实是各厂商API五花八门。
- CMT声明式事务有缺陷:CMT的声明式事务很方便好用,但是却绑定了EJB容器。
对应的,我们有了以下需求:
- 解耦事务管理和数据资源,对数据访问API进行合理抽象。
- 转译统一各种异常:包括各种场景下的受检和受检异常。
- 统一抽象事务管理编程模型:屏蔽各种事务管理API的差异,统一事务管理的编程模型。
- 声明式事务:摆脱EJB的限制,单独的使用声明式事务。
Spring就实现了以上需求。
提问
原生事务管理怎么做?缺点?直接拿具体数据访问厂商的API。事务和业务代码耦合在一起,很混乱。没有统一的异常处理。没有统一的API。单机的事务Java没有原生了,靠各个数据访问厂商提供。分布式的提供了JTA和JCA。
第19章 Spring事务王国的架构
Spring事务框架的好处和理念:
- 统一的编程模型来进行事务编程:我们使用统一的事务抽象API进行事务界定。不用再关心所使用的数据访问技术,不用关心具体要访问的事务资源类型。
- 紧密整合Spring自身的数据访问支持:Spring的事务框架与Spring的数据访问支持可以紧密结合,事务内要访问数据资源时,直接用Spring提供的数据访问API。
- 声明式事务:基于SpringAOP实现。
代码示例:
使用了统一的API,解耦和事务控制和数据访问资源,事务控制的对象(transactionManager)无需从数据访问资源(connection)获取
// 核心类:PlatformTransactionManager
// TransactionStatus
// TransactionDefinition
public class FooService {
private PlatformTransactionManager transactionManager;
public void serviceMethod() {
TransactionDefinition definition = ...;
TransactionStatus txStatus = getTransBactionManager().getTransaction(definition); // 事务控制代码
try {
// dao1.doDataAccess();
// dao2.doDataAccess();
// ···
} catch (DataAccessException e) {
getTransactionManager().rollback(txStatus); // 事务控制代码
throw e;
}
catch(OtherNecessaryException e) {
getTransactionManager().rollback(txStatus); // 事务控制代码
throw e;
}
getTransactionManager().commit(txStatus); // 事务控制代码
}
public PlatformTransactionManager getTransactionManager(){
return transactionManager;
}
public void setTransactionManager(PlatformTransactionManager transactionManager){
this.transactionManager = transactionManager;
}
由上代码可看出,核心类:
- PlatformTransactionManager
- TransactionStatus
- TransactionDefinition
19.1 如何实现一个PlatformTransactionManager实现类
org.springframework.transaction.
PlatformTransactionManager:Spring事务抽象架构的核心接口,为应用程序提供事务界定的统一方式。
声明如下:
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws Transaction Exception;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
整个事务架构,其实就是实现各种PlatformTransactionManager
的实现类。下面我们先来看下,如果自己实现,怎么实现。
我们用JDBC数据访问方式,来作为例子。
我们知道,JDBC的事务管理,是通过connection来的(connection.commit())。所以要保证所有的事务操作,用到的是同一个connection。
那么第一个思路是,可以把connection作为各方法间的形参来传递。但是这样比较蠢,不但事务管理代码耦合了数据访问代码,而且事务的操作和具体的数据访问方式(这里是JDBC的connection)耦合了。如下图:
那么另一个思路:把connection放到统一的地方去。这里就是放到TransactionResourceManager。
具体实现,把connetion绑定到当前线程:
- 在事务开始之前取得一个java.sql.Connection,将这个Connection绑定到当前的调用线程。
- 数据访问对象在使用Connection进行数据访问的时候,直接从当前线程上获得刚才绑定的Connection实例。
- 完成了数据访问工作后,继续用这个connection实例提交或者回滚事务。最后然后解除它到当前线程的绑定。
TransactionResourceManager
实现示例(简化代码):
public class TransactionResourceManager {
private static ThreadLocal resources = new ThreadLocal();
public static Object getResource() {
return resources.get();
}
public static void bindResource(Object resource) {
resources.set(resource);
}
public static Object unbindResource() {
Object res = getResource();
resources.set(null);
return res;
}
}
然后我们实现JDBC的PlatformransactionManager
实现类。在事务开始的时候,通过我们的TransactionResourceManager将java.sql.Connection绑定到线程,然后在事务结束的时候解除绑定即可:
public class JdbeTransactionManager implements PlatformrransactioManager {
private DataSource dataSource;
public JdbcTransactionManager(DataSource dataSource) {
this.dataSource= dataSource;
}
public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
Connection connection;
try {
connection = dataSource.getConnection();
TransactionResourceManager.bindResource(connection); // 把connection绑定到TransactionResourceManager
return new DefaultTransactionStatus(connection, true, true,false, true,null);
} catch (SQLException e) {
throw new CannotCreateTransactionException("can't get connection for tx",e);
}
}
public void rollback(TransactionStatus txStatus) throws TransactionException {
Connection connection = (Connection)TransactionResourceManager.unbindResource(); // 解绑connection
try {
connection.rollback();
} catch(SQLException e) {
throw new UnexpectedRollbackException("rollback failed with SQLException", e);
} finally{
try{
connection.close();
} catch(SQLException e){
//记录异常信息,但通常不会有进一步有效的处理
}
}
}
public void commit(TransactionStatus txStatus) throws TransactionException {
Connection connection = (Connection)TransactionResourceManager.unbindResource(); // 解绑connection
try {
connection.commit();
} catch(SQLException e){
throw new TransactionSystemException("commit failed with SQLException",e);
} finally {
try {
connection.close();
} catch (SQLException e) {
//记录异常信息,但通常不会有进一步有效的处理
}
}
}
以上我们用TransactionResourceManager
统一管理connection,完成了事务管理与响应数据访问对象(即connection)的耦合,以及具体数据访问对象接口定义的耦合。不过由于是简化代码,有几点还未考虑到:
- 如何保证
PlatformrransactionManager
的相应方法以正确的顺序被调用?后续给出答案 - 强制使用TransactionResourceManager来获取数据资源接口(connection)。如果不想用它呢?Spring数据访问框架的
org.springframework.jdbc.datasource.
DataSourceUtils工具类还提供了connection管理功能(之前是说它被用来做异常转译)。- connection资源获取位置:DatasourceUtils会从类似TransactionResqurceManager的类(Spring中对应org.springframework.transaction.support.
TransactionSynchronizationManager
)那里获取Connection资源。 - 如何保证connection唯一性:如果当前线程之前没有绑定任何connection,那么它就通过数据访问对象的DataSource引用获取新的connection,否则就使用绑定的那个connection。
- 所以,当我们要使用Spring提供的事务支持的时候,必须通过DatasourceUtils来获取连接。JdbcTemplate等类内部已经使用DatasourceUtils来管理连接。
- connection资源获取位置:DatasourceUtils会从类似TransactionResqurceManager的类(Spring中对应org.springframework.transaction.support.
对应Hilemate的SessionFactoryUtils,对应JDO的PersistenceManagerFactoryUtils以及对应其他数据访问技术的Utils类,它们的作用与DatasourceUtils相似。
19.2 Spring事务体系的实现
Spring的事务抽象包括3个主要接口:
PlatformTransactionManager
:界定事务边界。TransactionDefinition
:定义事务相关属性,包括隔离级别、传播行为等。TransactionStatus
:负责事务开启之后到事务结束期间的事务状态。
关系如下图所示:
下面来进行一一介绍。
1.TransactionDefinition
主要定义了可以指定的事务属性,包括:
- 事务的隔离(Isolation)级别
- 事务的传播行为(Propagation Behavior)
- 事务的超时时间(Timeout)
- 是否为只读(ReadOnly)事务
1.事务隔离级别
- ISOLATION_DEFAULT:使用数据库默认的隔离级别,一般是Read Committed。
- ISOLATION_READ_UNCOMMITTED:读未提交。可导致脏读、不可重复读、幻读。
- ISOLATION_READ_COMMTITTED。读已提交。可导致不可重复读、幻读。
- ISOLATION_REPEATABLEREAD。可重复读。可导致幻读。
- ISOLATION_SERIALIZABLE。序列化。可避免所有问题,但是性能很差。
2.事务的传播行为
说的是A()方法是一个事务,当A()内调用到B()时,事务以什么方式传播到B()事务。
一下语义,默认A()方法 -> 调用B()方法,然后用B()方法的视角解释:
- PROPAGATION_REQUIRED:需要。
- A()开启了事务:直接加入。
- A()没有开启事务:自己新建一个事务。
- B()抛异常:A()能感知到,且B会回滚。
- PROPAGATION_SUPPORTS:支持。
- A()开启了事务:直接加入。
- A()没有开启事务:直接执行。
- 适用场景:B()可以读到A()还未提交的数据更改。
- PROPAGATION_MANDATORY:强制。
- A()开启了事务:直接加入。
- A()没有开启事务:报错。
- PROPAGATION_REQUIRES_NEW:需要、新建。
- A()开启了事务:挂起A(),自己新建一个事务。
- A()没有开启事务:自己新建一个事务。
- 适用场景:B()的结果不想影响到外层事务。如B()写日志,即使报错了,也不想影响A()的执行。A()执行失败回滚时,B()不会回滚。
- PROPAGATION_NOT_SUPPORTED:不支持。
- A()开启了事务:挂起A(),非事务方式执行B()。
- A()没有开启事务:直接执行。
- PROPAGATION_NEVER:从不需要。
- A()开启了事务:抛异常。
- A()没有开启事务:直接执行。
- PROPAGATION_NESTED:嵌套。
- A()开启了事务:新建一个事务作为A的子事务嵌套执行,与外层事务共有事务状态。
- A()没有开启事务:自己新建一个事务。
- 适用场景:将一个大的事务划分为多个小的事务来处理,并且外层事务可以根据各个内部嵌套事务的执行结果,来选择不同的执行流程。比如A()调用B()插入数据,由于B()主键冲突执行失败回滚,此时A可以改为再调用C()更新数据。A()执行失败回滚了,所有子事务都回滚。内层事务依赖于外层事务,内层事务失败外层事务不回滚,外层事务失败,内层事务全部回滚。
下面这张表更好看一点:
上述最常用的是PROPAGATION_REQUIRED
,一般也是默认的传播行为。在考虑要选择哪种传播行为时,从几个点考虑:
- 自己是否回滚:即自己是否是事务?
- 两者是否相互影响:即A()或B()执行失败了?B()或A()是否回滚?
- B()是否能读到A()未提交的数据?(DB隔离级别大于等于读已提交的情况下)
3.事务的超时时间
TransactionDefinition提供了TIMEOUT_DEFAULT
常量定义超时时间,默认-1,此时采用当前事务系统的默认超时时间。
4.是否只读事务
只读的事务仅仅是给相应的ResourceManager(即数据库)提供一种优化的提示,但最终是否提供优化,则由具体的ResourceManager来决定。
5.TransactionDefinition相关实现
TransactionDefinition
的相关实现类主要分为编程时事务和声明式事务两类。
如下图所示:
各个类解释如下:
- DefaultTransactionDefinition:TransactionDefinition接口的默认实现类。提供了事务各属性的默认值,并可通过setter()方法设置。
- propagationBehavior = PROPAGATION_REQUIRED
- isolationLevel = ISOLATION_DEFAULT
- timeout =TIMEOUTDEFAULT
- readOnly= false
- TransactionTemplate:进行编程式事务管理的模板方法类。所以使用它时可以直接通过自己获取到事务属性。
- TransactionAttribute:主要面向使用Spring AOP进行声明式事务管理的场景,增加了
boolean rollbackOn(Throwable ex);
方法来支持用注解声明要抛出的异常。 - DefaultTransactionAttribute:同时继承了两个接口,
- RuleBasedTransactionAttribute和DelegatingTransactionAttribute:允许指定多个回滚规则,根据不同的异常来决定不同的回滚行为。
- DelegatingTransactionAttribute:用于被子类化,将所有方法调用委派给另一个具体的TransactionAttribute实现类。
2.TransactionStatus
org.springframework.transaction.TransactionStatus接口表示事务状态,通常在编程式事务中使用。可以实现:
- 查询事务状态
- 标记当前事务
- 如果相应的PlatformTransactionManager支持Savepoint,可以通过TransactionStatus在当前事务中创建内部嵌套事务。
如下图所示:
- DefaultTransactionStatus:Spring事务框架内的各个TransactionManager的实现,大都借助于DefaultTransactionstatus来记载事务状态信息。
- SimpleTransactionStatus:在Spring内部实现中没有使用到。
3.PlatformTransactionManager
具体的实现思路,前文已经经过了。这里主要讲整个PlatformrransactionManager的层次体系,以及各个实现类。
PlatformTransactionManager
的整个抽象体系基于Strategy模式,由PlatformTransactionManager对事务界定进行统一抽象,具体的界定策略的实现则由具体的实现类来实现。
1.PlatformrfransactionManager实现类概览
可以分为局部事务和分布式事务两个分支。
面向局部事务的实现类,如下图所示:
面向全局事务的实现类,如JtaTransactionManager等。具体略。
2.DataSourceTransactionManager实现类的具体实现(暂略待补充)
PlatformrfransactionManager实现类的结构和理念都大同小异,我们选择DataSourceTransactionManager作为例子来分析下。
提问
Spring事务框架实现的核心思路?1.PlatformTransactionManager,TransactionStatus,TransactionDefinition。2.(1)核心是在TransactionManager要把connection存下来,然后所有事务操作用同一个connection来操作(用ThreadLocal的方式实现)。(2)不想用它的话,可以用DataSourceUtils
替代。Spring提供的事务支持的时候,就是通过DatasourceUtils来获取连接(3)可以用形参在方法间传导connection的方式,不过太蠢,而且跟具体的connetion耦合了。3.另外,Spring数据访问框架的DataSourceUtils提供了获取同一个connection的逻辑,所以我们都要用他。
Spring事务架构涉及哪些接口?各用来干啥?如何实现?
事务传播机制是解决什么问题的?有哪些?https://blog.csdn.net/weixin_39625809/article/details/80707695
DataSourceTransactionManager怎么实现的?如何判断事务传播机制?提交后做哪些处理?
第20章 使用Spring进行事务管理
本章主要讲,Spring的事务管理怎么用。分编程式事务管理和声明式事务管理。
20.1 编程式事务管理
1.直接使用PlatformTransactionManager 进行编程式事务管理
PlatformTransactionManager
接口定义了事务界定的基本操作,可以直接用它来管理:
// 1.definition
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setTimeout(20);
// ...
// 2.transactionStatus
TransactionStatus txStatus = transactionManager.getTransaction(definition);
try {
// 业务逻辑实现
} catch(Error e) {
// 3.rollback
transactionManager.rollback(txStatus);
throw e;
}
// 3.commit
transactionManager.commit(txStatus);
PlatformTransactionManager
已经屏蔽了不同事务管理API的差异,所以直接用它来管理没问题。
不过还是过于贴近底层,且由大量重复代码。
我们可以参照Spring数据访问层一样,用模板方法模式 + CallBack来管理事务,也就是用TransactionTemplate的编程式事务管理。
2.使用 TransactionTemplate进行编程式事务管理
TransactionTemplate txTemplate = ..;
Object result = txTemplate.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus txStatus) {
Object result = null; // 各种事务操作…
return result;
}});
此外还可以处理具体的抛出异常,或是回滚/提交方式。详细略。
3.编程创建基于Savepoint 的嵌套事务
TransactionStatus不但可以在事务处理期间通过setRollbackonly()方法来干预事务的状态,如果需要,作为SavepointManager,它也可以帮助我们使用Savepoin机制来创建嵌套事务。
Object savePolintBeforeDeposlt = transactionStatus.createSavepoint();
try {
// ...
} catch (Exception ex) {
transactionStatus.rollbackToSavepolnt(savePointBeforeDeposit);
} finally {
transactionStatus.releaseSavepolnt(savePointBeforeDeposit);
}
详细略。
20.2 声明式事务管理
1.实现思路
提供一个拦截器,在业务方法执行开始之前开启一个事务,当方法执行完成或者异常退出的时候就提交事务或者回滚事务。用ApringAOP实现即可。
public class PrototypeTransactionInterceptor implements MethodInterceptor {
private PlatformTransactionManager transactionManager;
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
TransactionDefinition definition= getTransactionDefinitionByMethod(method);
TransactionStatus txStatus =transactionManager.getTransaction(definition);
Object result =null;
try {
result = invocation.proceed();
} catch(Throwable t) {
if(needRollbackOn(t))
transactionManager.rollback(txStatus);
} else {
transactionManager.commit(txStatus);
}
throw t;
transactionManager.commit(txStatus);
return result;
}
protected boolean needRollbackOn(Throwable t) {
// TODO.更多实现细节
return false;
}
protected TransactionDefinition geTransactionDefinitionByMethod(Method method) {
// TODO...更多实现细节
return null;
}
public PlatformTransactionManager getTransactionManager() { return transactionManager; }
public void setTransactionManager(PlatformrransactionManager transactionManager) { this.ransactionManager = transactionManager; }
}
还有两个元数据信息需要提供,可以写死在拦截器,更好的方法是放到XML,Spring允许我们在IoC容器的配置文件中直接指定事务相关的元数据:
对每个对象业务方法的拦截,需要知道该方法是否需要事务支持。如果需要,针对该事务的TransactionDefinition相关信息又从哪里获得?
如果调用方法过程中抛出异常,如何对这些异常进行处理,哪些异常抛出的情况下需要回滚事务,哪些异常抛出的情况下又不需要?
2.XML元数据驱动的声明式事务
即用XML来设置元数据存放在哪。
Spring1x到2x,大体上来说,我们可以使用以下4种配置方式在IoC容器的配置文件中指定事务需要的元数据。
- 使用ProxyFactory(ProxyFactoryBean)+TransactionInterceptor。
- 使用”一站式”的TransactionProxyFactoryBean。
- 使用BeanNameAutoProxyCreator。
- 使用Spring 2.x的声明事务配置方式。
详细略。
3.注解元数据驱动的声明式事务
直接用@Transactional声明即可:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default{};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default{};
}
注解的底层实现相当于下面的代码,就是用TransactionTemplate + CallBack来实现:
// ...
public Quote getQuate() {
try {
Method method = quoteService.getClass().getDeclaredMethod("getQuate", null);
boolean isTxAnnotationPresent = method.isAnnotationPresent(Transactional.class);
if (!isTxAnnotationPresent) {
return (Quote)quoteService.getQuate();
}
Transactional txInfo = method.getAnnotation(Transactional.class);
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
if(!txInfo.propagation().equals(Propagation.REQUIRED)) {
transactionTemplate.setPropagationBehavior(txInfo.propagation().value());
}
if(txInfo.readonly()) {
transactionTemplate.setReadonly(true);
}
//..
return (Quote)transactionTemplate.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus txStatus) {
return quoteService.getQuate();
}
});
} catch(SecurityException e) {
e.printStackTrace();//不要这样做
return null;
} catch (NoSuchMethodException e) {
e.printStackTrace();//不要这样做
return null;
}
}
在配置下注解即可:
<tx:annotation-driven transaction-manager="transactionManager"/>
提问
Spring声明式事务怎么实现?TransactionDefinition等元数据存到哪?怎么存?存XML或者@Transactional注解里。
@Transactional注解底层如何实现?TransactionTemplate + CallBack。
第21章 Spring事务管理之扩展篇
本章内容:
- 理解并活用ThreadLocal
- 谈Strategy模式在开发过程中的应用
- Spring与JTA背后的奥秘
21.1 理解并活用 ThreadLocal
1.ThreadLocal用来做什么
ThreadLocal:Java语言提供的用于支持线程局部变量(thread-local variable)的标准实现类。
2.ThreadLocal如何实现
ThreadLocal本身并不保存数据,数据是由线程自己来保存的。
// 伪代码举例,不严谨
public class Thread {
ThreadLocal.ThreadLocalMap threadLocals;
}
// ThreadLocal设置线程本地变量
Thread thread = Thread.currentThread();
ThreadLocalMap threadlocalmap = thread.threadLocals;
threadLocalMap.set(this, obj);
如上代码所示,通过ThreadLocal设置的变量,是保存到了每个线程的threadLocals上。
如下,通过ThreadLocal
的set(data)方法来设置数据,具体步骤:
- 获取被设置线程的threadLocals句柄:ThreadLocal会首先获取当前线程的引用,然后通过该引用获取当前线程持有的threadLocals
- 以自己为key存入数据:以当前ThreadLocal作为Key,将要设置的数据设置到当前线程,
get()之类的方法,基本上也是一样的,都是首先取得当前线程,然后根据每个方法的语义,对当前线程所持有的threadLocals中的数据进行操作。
所以实际上ThreadLocal
是被多个线程共用的。
3.ThreadLocal的应用场景
- 线程安全:为每个线程复制一份数据副本,避免多线程共享。
- 实现当前程序执行流程内的数据传递:用于替代全局变量或形参传输方式。例如可通过ThreadLocal来跟踪保存在线程内的日志序列。或前面的事务connection传递,保证所有的事务控制都是用的同一个connection。
- 某些情况下的性能优化:非共享模式。
- per-thread Singleton:如果某个资源创建代价大,而且后面会多次访问,就可以用ThreadLocal绑定到具体线程流程。相当于替代了全局变量形式。
4.使用 ThreadLocal管理多数据源切换的条件
在多数据源的切换过程中,切换的条件可能随着应用程序的需求而各异,可能要外部条件的介入,这就会有一个问题,如何为AbstractRoutingDatasource
的实现子类传入这些外部条件相关的数据?ThreadLocal这个时候就可以派上用场。
思路,如何快速切换数据源:通过ThreadLocal保存每个数据源所对应的标志(该标志我们以枚举类的形式给出),AbstractRoutingDataSource在通过determineCurrentLookupKey()获取对应数据源的键值的时候,直接从mreadLocal获取当前线程所持有的数据源对应标志然后返回。
// 1.枚举数据源标志
public enum DataSources {
MAIN,INFO,DBLINK;
}
// 2.DataSourceTypeManager持有ThreadLocal
public class DatasourceTypeManager {
private static final ThreadLocal<Datasources> dsTypes = new ThreadLocal<Datasources>(){
@override
protected Datasources initialValue() {
return DataSources.MAIN;
}
};
public static DatasSources get() {
return dsTypes.get();
}
public static void set (DataSources dataSourceType) {
dsTypes.set(datasourceType);
}
public static void reset() {
dsTypes.set(DataSources.MAIN);
}
}
// 3.实现AbstractRoutingDatasource
public class ThreadLocalVariableRountingDatasSource extends AbstractRoutingpatasource {
@0verride
protected Object determineCurrentLookupKey() {
return DataSourceTypeManager.get();
}
}
// 4.要将ThreadLocalVariableRountingDataSource以及相关的依赖注册到loC容器.
// XML略
// 5.数据源切换示例
DataSourceTypeManager.set(DataSources.INFO);
//或者
DataSourceTypeManager.set(DataSources.DBLINK);
21.2 谈 Strategy模式在开发过程中的应用
Strategy模式:本意是封装一系列可以互相替换的算法逻辑,使得具体算法的演化独立于使用它们的客户端代码。
实际上我们不应该只着眼”算法”一词。实际上,只要能够有效地剥离客户端代码与特定关注点之间的依赖关系,Strategy模式就应该进入考虑之列。
Spring框架中使用Strategy模式举例:
- 事务抽象框架:通过将使用不同事务管理API进行事务管理的界定行为进行统一的抽象,客户端代码可以以透明的方式使用
PlatformTransactionManager
这一策略接口进行事务界定,即使具体的事务策略需要变更,对于客户端代码来说也不会造成过大的冲击。 - 在IoC容器根据bean定义的内容,实例化相应bean对象的时候,会根据情况决定使用反射还是使用CGLIB来实例化相应的对象。InstantiationStrategy是容器使用的实例化策略的抽象接口,Spring框架默认提供了CglibSubclassingInstantiationStrategy和SimpleInstantiationStrategy两个具体实现类。
- Spring的Validation框架中,org.springframework.validation.Validator定义也是一个策略接口,具体的实现类将根据具体场景提供不同的验证逻辑。而这些具体验证逻辑的差异性,对于使用validator进行数据验证的客户端代码来说,则是透明的。
详细略。
21.3 Spring与 JTA 背后的奥秘
用Spring的JtaTransactionManager进行分布式事务管理的时候,都强调需要使用从应用服务器的JNDI服务获取的dataSource, 而不是本地配置的普通datasource。
那么原因是什么呢?
- 首先,具体的事务资源,RDBMS、MessageQueue等,要加入JTA管理的分布式事务,JTA规范要求其实现javax.transaction.xa.XAResource接口。
- 想要参与JTA分布式事务的事务资源拥有了XAResource支持之后,JTA的javax.transaction.TransactionManager(我们称其为JTA TransactionManager,区别于Spring的JtarransactionManager)与RM之间就可以进行通信。
- 适配器通常都有应答能力,这样,在JTATransactionManager使用两阶段提交协议管理分布式事务的过程中,可以同每个RM进行交互。
- TATransactionManager与各个RM之间的联系要由Applicationserver(一般意义上的TPMonitor)来进行协调!Applicationserver为基于JTA的分布式事务提供运行时环境,并负责协调JTATransactionManager与各RM之间的交互。
详细略。
提问
ThreadLocal用来做什么?实现原理?获取当前线程的threadLocals变量,然后以自己为key,存入变量。
ThreadLocal应用场景?
策略模式在Spring使用的示例?
第6部分 Spring的Web MVC框架
第22章 Spring MVC 演进历史
1.Servlet
Servlet,提供了Session和对象生命周期管理等功能。
最早期,就只有Servlet。只用Servlet的问题:
- Servlet太重,什么东西都往里面塞,业务逻辑耦合在一起,难以维护:包括流程控制逻辑、视图显示逻辑、业务逻辑、数据访问逻辑等。比如一堆的
out.printiln
输出网页字符。
2.JSP
JSP有啥用:Servlet中,后台业务和网页字符混杂在一起,一堆的out.printiln
难以入目,所以引入了JSP。
JSP,就是把视图渲染逻辑抽出来。
JSP最终也是编译成Servlet来运行。所以,可以直接在JSP里面写Java代码,还可以用servlet处理web请求。
最终JSP也发展得很臃肿。JSP已经不再是一个单纯的视图模板。
JSP Model 1:为了封装业务逻辑,引入了JavaBean,但是JSP还是很臃肿。
3.Servlet + JSP
1.JSP Model 2
JSP Model 2:痛定思痛,我们决定,JSP就只做JSP的事情,即视图渲染。已经初具MVC的样子。
虽然JSP Model 2 已经具备了使用MVC模式实现的Web应用架构的雏形,但并非严格意义上的MVC。为了搞清楚其间的差别,我们先来简单回顾一下MVC模式以及模式中涉及的几个组件。
2.MVC
MVC,即Model-View-Controler,模型-视图-控制器:
- 控制器:接收视图发送的请求,并处理。之后选择合适的视图显示给用户。
- 模型:封装应用逻辑和数据状态。
- 视图:面向用户的接口。
3.使用单个servlet控制器还是多个:
上述JSP Model 2 模型,已经很接近MVC模型。但是还是没用规定控制器怎么用:
使用多个servlet作为控制器:最初时,使用最多的模式。缺点是一个请求一个Servlet,管理分散混乱、web.xml膨胀的很厉害
使用单个servlet作为控制器:后期基本都是用这个模式。Servlet作为集中控制器。不过避免了web.xml文件的膨胀,却将这种膨胀变相地带到了Servlet控制器类中。控制器类需要自己来做URL请求到具体逻辑的映射。
如我们所看到的,制约JSPModel2发展的,就是将流程控制等通用相关逻辑进行硬编码的实践方式,这直接导致了JSPModel2架构的不可重用性。
4.Web框架
Web框架的意义,就是抽象了可统一处理的通用逻辑。让开发人员把精力真正的放到业务开发上。
Web框架分为两类:
- 请求驱动型:由JSP Model2进化。典型的就是Struts、SpringMVC。
- 事件驱动型:将视图组件化,由视图中的相应组件触发事件,进而驱动整个处理流程。如JSF(Java Server Face),很少用了。
Web框架的演进:具体来说,就是原来Servlet是作为单一的控制器。现在,Servlet作为Front Controller,和次级控制器类共同组成了整个应用程序的控制器。该Servlet接收到具体的Web处理请求之后,会参照预先可配置的映射信息,将待处理的Web处理请求转发给次一级的控制器(sub-controller)来处理。
小结
SpringMVC的演进:
- 单一的Servlet:全能的Servlet。
- JSP Model 1:JSP + JavaBean。把视图逻辑、业务逻辑分开。JSP本质也是编译成Servlet。
- JSP Model 2:Servlet + JSP + JavaBean 。把控制逻辑、视图逻辑、业务逻辑分开。
- Web框架:Servlet单一的控制器 + 次级控制器。 抽出了URL分派的硬编码,统一使用一个Servlet作为前置控制器,整个框架由两层控制器来组成。
提问
MVC指的是什么?Servlet + JSP + JavaBean
SpringMVC的演进?单一的Servlet -> JSP Model 1:JSP + JavaBean -> JSP Model 2:Servlet + JSP + JavaBean -》Web框架:抽出硬编码,Servlet单一的控制器 + 次级控制器。
第23章 整体概括SpringMVC
Spring MVC的设计优点:
Web层:即控制器实现,对请求处理期间涉及的各个关注点进行了合理分离:
- 设置HandlerMapping:处理匹配Web请求与具体请求处理控制器之间的映射;
- 设置LocaleResolver:国际化处理;
- 设置ViewResolver:灵活的视图选择等。
表现层:运用逻辑命名视图(logical named view)策略,通过引入
ViewResolver
和view
,清晰地分离了视图类型的选择和渲染(Render)与具体控制器之间的耦合。JSP/JSTL、Velocity/FreeMarker,甚至PDF/Excel等二进制格式的视图形式,都可以简单的整合。生态好:天生支持Spring的AOP、IOC等支持。
1.Spring MVC总览
两层控制器设计:Dispatcherservlet处理所有Web请求,再委派到下一级控制器Controller去实现:
DispatcherServlet的处理流程,简单概括如下:
- HandlerMapping:Web请求到达
DispatcherServlet
之后,Dispatcher通过具体的HandlerMapping
实例,来获取当前Web请求的具体Controller
。 - Controller:DispatcherServlet 调用 HandlerMapping 返回 Controller,再把请求发到Controller。Controller执行完毕后,返回
ModelAndview
,包含以下两部分信息:- 视图的逻辑名称(或者具体的视图实例)。DispatcherServlet将根据该视图的逻辑名称,来决定为用户显示哪个视图。
- 模型数据。用于视图渲染时并入到视图显示。
- ViewResolver和View:ViewResolver用来映射视图名和View实例。View用来封装各种视图处理实例。
以下是SpringMVC各角色交互图:
2.SpringMVC使用示例(略)
小结
SpringMVC的整体流程。如图23-3所示。
提问
SpringMVC怎么处理一个请求?见各角色交互图。同时参照第25章两个图。
第24章 SpringMVC详细解析
SpringMVC的核心组价,包括:
- HandlerMapping
- Controller
- ModelAndview
- ViewResolver
- View
以上5个主要的组件它们共同组成了SpringMVC框架的强大躯干。本章对它们进行了详细的介绍。
1.HandlerMapping
Handler:次级控制器的总称。Spring MVC,前置控制器是DispatcherServlet,次级控制器除了Spring提供的Controller外,还有Spring提供的其他次级控制器,或是其他第三方提供的控制器。它们统称Handler。
HandlerMapping:帮助DispatcherServlet进行Web请求的URL到具体处理类的匹配。
接口定义如下:
public interface HandlerMapping {
String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName()+"".pathwithinHandlerMapping";
// 注意到返回给DispatcherServlet的是HandlerExecutionChain,而不是Controller。不过它里面包含了Handler,后面详解
HandlerExecutionChain getHandler(IttpServletRequest request) throws Exception;
}
1.HandlerMapping的实现类
SpringMVC提供了多个默认实现类给我们使用:
- BeanNameUrlHandlerMapping:强制我们的Handler的bean定义名称,必须去匹配视图中的链接路径。
- SimpleUrlHandlerMapping:进一步解除了请求URL与Handlert的beanName之间的耦合,并且支持更灵活的映射表达方式。
- ControllerClassNameHandlerMapping:后面27章讲。
- DefaultannotationHandlerMapping:基于注解的方式。后面26章讲。
2.HandlerMapping 执行顺序
可以为DispatcherServlet提供多个HandlerMapping。执行时,按照优先级进行排序。
HandlerMapping实现全都实现了ordered接口。order越小越优先,默认值为最低优先级Integer.MAX_VALUE。
2.Controller
Controller:Spring MVC框架支持的用于处理具体Web请求的handler类型之一。
接口声明如下:
public interface Controller(
ModelAndView handleRequest(HttpServletRequest request,HttpServletrResponse response) throws Exception;
}
自定义的Controller:可以实现自定义的Controller,需要关注请求参数的抽取、请求编码的设定、国际化信息的处理、Session数据的管理等细节。
1.Controller实现类
Spring也提供了现成的Controller实现类:
上图中,Controller分类:
- 自由挥洒派的controller:只要熟悉Servlet API,就可以随意处理Web请求了。从HttpServletRequest中获取参数,然后验证,调用业务层逻辑,最终返回一个ModelAndView,甚至,你可以直接通过HttpServletResponse输出最终的视图。不过,要关心的细节比较多。
- 规范操作派的Controller:把某些通用逻辑进行了一些封装:
- 自动抽取请求参数并绑定到指定的Command对象。
- 提供了统一的数据验证方式:BaseCommandController及其子类可以接收一组org.springframework.validation.
Validator
以进行数据验证,我们可以根据具体数据提供相应的validator实现。 - 规范化了表单(Form)请求的处理流程,并且对简单的多页面表单请求处理提供支持。
自由派Controller:
MutiActionController
:一个Controller处理多个Web请求,如一个数据的增删查改请求。类似Struts 1.1框架的DispatchAction。请求参数到Comman对象的绑定、数据验证、异常处理等细节,也都一起完成。不用再每个请求都去实现一个AbstractController。
规范派Controller:
BaseCommandController
提供了自动数据绑定和通过validator的数据验证功能。AbstractFormController
:在BaseCommandConroller基础上,发展了一套模板化的form处理流程。至此,从数据的封装,验证,再到处理流程的模板化,整个规范化体系即告建立完成。SimpleFormController
:在AbstractFormController基础上,专门面向单一表单的处理。AbstractWizardFormController
:在AbstractFormController基础上,专门面向单一表单的处理提供多页面向导的交互能力。即,提供向导式的简单的多页面流程实现支持,例如要注册某个网站的会员,注册过程可能就包括多步,每一步会提示输入某一方面的信息,以帮助我们简化操作流程。
2.数据绑定
以SimpleFormController举例。
数据绑定:自动提取HttpServletRequest
中的相应参数,然后转型为需要的对象类型。我们唯一需要做的,就是为数据绑定提供一个目标对象,这个目标对象在Spring中称为*Command
对象*。所以我们不用再自己通过request.getParameter(String)方法遍历获取每个请求参数,然后根据需要转型为自己需要的类型了。
数据绑定的过程:
- 获取请求参数值集合:在Web请求到达之后,Spring MVC某个框架类将提取当前Web请求中的所有参数名称,然后遍历它,以获取对应每个参数的值,获取的参数名与参数值通常放入一个值对象(
Propertyvalue
)中。最终我们将拥有所有需要绑定的参数和参数值的一个集合(Collection
)。 - 设置到Command对象:根据command对象中各个域属性定义的类型进行数据转型,将参数值集合设置到
command
对象上。
BeanwrapperImpl会将Command对象纳入自身管理范围,参数值与Command对象属性间类型差异性的转换工作,则由Beanwrapperrmpl所依赖的一系列自定义PropertyEditor负责。也可以添加自定义的PropertyEditor。
command对象定义示例:
public class CustomerMetadata {
private String address; private String zipCode;
private List<PhoneNumber> phonenumbers = new ArrayList<PhoneNumber>();
public CustomerMetadata() {
phoneNumbers.add(new PhoneNumber());
}
}
// getter和setter方法定义
public class PhoneNumber{
private String areaCode;
private String number;
public String getAreaCode()(
return areaCode;
)
public void setAreaCode(String areaCode){this.areaCode = areaCode;}
public String getNumber() {return number;}
public void setNumber(String number){ this.number = number; }
// toString()等方法定义
}
// 参数名要和字段名一致。SimpleFormController参数按以下方式发送:
// <input type="text"name="address"/>
// <input type="text"name="zipCode"/>
// <input type="text"name="phoneNumbers[0].number"/>
3.数据验证
以SimpleFormController举例。
Spring框架提供的数据验证支持并不只是局限于Spring MVC内部使用。在其他地方也能用,引入包org.springframework.validation
即可。
Spring数据验证框架,核心类为org.springframework.validation.*包下的Validator
和Errors
,Validator
实现具体的验证逻辑,Errors
承载验证过程中出现的错误信息。二者通过validator接口定义的主要验证方法validate(target,errors)
集成。
Validator定义,具体功能需要我们自己写实现类:
public interface Validator{
boolean supports(Class clazz); // 匹配Valicator。因为各种验证可以用多个Validator实现。
void validate(Object target, Errors errors); // 验证,并把异常加入Errors
}
具体使用:
- 需要构造一个具体的Command对象实例(CustomerMetadata)以及一个Errors实例(BindException)
- 通过validationUtils调用对应的validator实现类(也可直接调用validator的validate()方法)。
- 调用完成之后,即数据验证完成,如果存在验证错误(可通过errors.hasErrors()获知),我们可以遍历之前传入的errors以获取相应的错误信息,然后根据具体应用程序的场景做后继处理。
Spring还要其他类型的Controller实现类,如AbstractCommandContxoller、ParameterizableViewController、UrlFilenameViewController、ServletForwardingController和ServletWrappingController。不再详述。
其他方法,有声明式数据验证。现在用得最多。
3. ModelAndView
Controller在将Web请求处理完成后,会返回一个ModelAndview实例,或者返回null(表示controller内部自行处理视图的渲染)。
ModelAndview
实例包含两部分内容:
- 视图:逻辑视图名称,或是具体的view实例;
- 返回view实例:DispatcherServlet将直接从ModelAndview中获取该View实例并渲染视图。
- 返回视图名称:DispatcherServlet**通过
ViewResolver
**,获得一个view,最终渲染输入。为了保证灵活性,除非必要,尽量通过逻辑视图获取,不要直接返回具体的view实例。
- 模型数据:以org.springframework.ui.
ModelMap
的形式来保持模型数据。视图渲染过程中将会把这些模型数据合并入最终的视图输出。
4.ViewResolver
ViewResolver
:视图定位器。根据controller所返回的ModelAndaview中的逻辑视图名,为DispatcherServlet返回一个可用的view实例。
接口定义如下:
public interface viewResolver {
View resolveviewName(String viewName, Locale locale)throws Exception;
}
我们可以自己实现ViewResolver。Spring也提供了现成的实现类。
1.实现类
主要有以下:
- 面向单一视图的定位器。根据位置返回view示例。都直接或间接继承自UrlBasedviewResolver。无需要为它们配置具体的“逻辑视图名到具体view”的映射。通常只要指定视图模板所在的位置,这些viewResolver就会按照逻辑视图名,抓取相应的模板文件、构造对应的view实例并返回。每个具体的ViewResolver实现都只负责一种view类型的映射,ViewResolver与View之间的关系是一比一,所以叫面向单一视图的定位器。
- InternalResourceviewResolver:处理JSP模板类型的视图映射。
- FreeMarkerViewResolver/VelocityviewResolver:分别处理FreeMarkerView和velocityview类型视图的查找和构建。
- JasperReporteViewResolver:查找并返回JasperReport类型的view实例
- BltViewResolver。查找并返回XsltView类型的view实例。
- 面向多视图类型的定位器。要通过某种配置方式明确指定逻辑视图名与具体视图之间的映射关系,好处是可以顾及多种视图类型的映射管理。
- ResourceBundleViewResolver:唯一提供视图国际化支持的ViewResolver。properties配置格式。
- XmlViewResolver:xml配置信息。默认会加载/WEB-INF/views.xml作为配置文件。
- BeanNameViewResolver:XmlViewResolver的简化版,直接将view实例注册到当前Dispatcherservlet中。用于快速验证,一般生产不用。
2.查找顺序
DispatcherServlet不但可以接受多个HandlerMapping以处理Web请求到具体Handler的映射,也可以接受多个ViewResolver以处理视图的查找。
ViewResolver的优先级的指定使用ordered接口作为标准。
DispatcherServlet根据这些ViewResolver的优先级进行排序,按顺序查找,知道找到一个不返回null的定位器。如果都没找到,则默认使用InternalResourceviewResolver。
5.View
接口定义如下:
public interface View{
String getContentType();
void render(Map model, HEttpServletRequest request, HttpServletResponse response) throws Exception;
}
关键就是render方法。
1.原理
就是把数据和返回值的合并,以及返回客户端的动作,抽出来而已。
举例:
public class ExcelView implements View {
private String xlsTemplateLocation;
public string getContentType() {return "application/vnd.ms-excel"; }
public void render(Map model,HttpServletRequest request, HittpServletResponse response) throws Exception {
response.setContentType(getContentType());
//1.定位模板位置
HSSFWorkbook workbook =readInExceiTemplate(xlsTemplateLocation);
// 2.合并数据和模板
mergeModelwithTemplate(model, workbook);
//3.输出到客户端
ServletOutputStream out = response.getOutputStream();
workbook.write(out);
out.flush();
}
private void mergeModelWithTemplate(Map model,HSSFWorkbook workbook) {
workbook.=getSheetAt(1).getRow(11).getCell(short)1).setCellValue((String)model.get("dataKey"));
}
protected HSSFWorkbook readInExcelTemplate(String location) throws Exception{
File ×lsFile= new File(location);
InputStream ins = new FileInputStream(xlsPile);
POIFSFileSystem fs = new POIFSFilesystem(ins);
HSSFWorkbook workbook = new HSSFWorkbook(fs); return workbook;
// getter和stter方法定义
}
2.实现类
Spring MVC提供的view实现类都直接或者间接继承自org.springframework.web.servlet.view.AbstracteView
。提供了以下几个公共字段:
- private String contentType = DEFAULT_CONTENT_TYPE。默认ISO-8859-1。
- private String requestContextAttribute。RequestContext对应的属性名。
- private final Map staticAttributes=new HashMap()。保存视图的静态属性,如页眉、页脚的固定信息等。
view有用JSP实现的、通用模板实现的、二进制实现的、XSLT技术实现的、重定向实现等等,具体实现类略。
通常的ViewResolver实现都继承了AbstractviewResolver的默认开启缓存功能。
3.自定义实现
略。
第25章 更多SpringMVC组件支持
主要内容:
- 文件上传与MultipartResolver
- Handler与HandlerAdaptor
- 框架内处理流程拦截与HandlerInterceptor
- 框架内的异常处理与HandlerExceptionResolver
- 国际化视图与LocalResolver
- 主题(Theme)与ThemeResolver
SpringMVC整体流程如下图:
加入更细节的处理后,流程如下图,注意和上图对比:
在上图中,老的“骨架”保留,新加的细节进行了标注。
- MultipartResolver:用于处理文件上传请求。
- HandlerInterceptor。对处理流程进行拦截。拦截的位置可以有三个地方可以选择(斜线背景的竖向方框所标志的位置)。
- HandlerAdaptor。可以帮助我们使用其他类型的Handler,不只是支持Controller。向DispechServlet屏蔽不同类型的Handler。
- HandlerExceptionResolver。在处理具体Web请求的过程中,相应的Handler出现异常时,HandlerExceptionResolver提供了一种框架内的标准处理方式。
- LocaleReaolver。根据用户的Locale显示不同的视图。
- ThemeResolver。让用户可以选择不同的主题(Theme)。
1.MultipartResolver
1.处理流程
MultipartResolver:主要用于在服务器端处理文件上传,前端通过表单上传文件。
MultipartResolver接口提供了上传文件的抽象,通过它我们可以选择各种不同的上传类库。
接口定义如下:
public interface MultipartResolver{
boolean isMultipart(HttpServletRequest request); // 查看当前请求是否是multipart类型
// 处理HttpServletRequest,替换为子类MultipartHttpServletRequest返回
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
void cleanupMultipart(MultipartHttpServletRequest request); // 流程处理完毕后,DispatcherServlet调用该方法释放系统资源。
}
处理流程如下:
检查
webApplicationContext
中有没有multipartResolver
对象:Web请求到达Dispatcherservlet并等待处理的时,Dispatcherservlet检查能否从自己的webApplicationContext中找到一个名称为multipartResolver(由DispatcherServlet的常量MULTIPART_RESOLVER_BEAN_NAME所决定)的MultipartResolver实例。如果有,检查当前web请求类型是否为
multipart
类型:DispatcherServlet将通过MltipartResolver的isMultipart(request)方法检查- 如果是,返回一个
MultipartHttpServletRequest
供后继处理流程使用:DispatcherServlet调用的是MltipartResolver的resolveMultipart(request)方法 - 如果不是,则返回最初的
HttpServletRequest
- 如果是,返回一个
没有,则不作处理。继续使用HttpServletRequest。
MultipartHttpservletRequest
是HttpservletRequest
的子类,所以相当于是被偷梁换柱了。定义如下:
public interface MultipartHttpservletRequest extends HttpservletRequest, MultipartRequest{}
public interface MultipartRequest {
Iterator getFileNames();
MultipartFile getFile(String name);
Map getFileMap();
}
2.MultipartResolver实现类
实现类是以下两个,本质上只是两个不同的实现类库而已。因为是有标准的,所以类库不一样,但是他们遵守的标准一样,问题不大:
- org.springframework.web.multipart.commons.CommonsMultipartResolver:使用Commons FileUpload类库实现
- org.springframework.web.multipart.cos.CosMultipartResolver:使用Oreilly Cos类库实现。
MultipartResolver返回MultipartHttpServletRequest给后继处理流程后,后继处理流程中的组件(通常是相应的Controller)继续使用MhltipartHttpservletRequest处理相应的Web请求。
处理完Web请求后,DispatcherServlet将保证调用MultipartResolver的cleanupultipart(...)
方法,释放处理文件上传过程中所占用的系统资源。至此,整个文件上传的生命周期结束。
具体使用时,添加一个MultipartResolver的实例到DispatcherServlet的webApplicationContext中即可。
具体实现略。
为了在数据绑定过程中数据能够成功转型,我们需要为DataBinder添加相应的自定义PropertyEaitor实现。覆写SimpleFormController的initBinder(…)方法即可。
Spring MVC默认提供了两个自定义PropertyEditor实现类来处理数据绑定:
- org.springframework.web.multipart.support.ByteArrayMultipartFileEditor:负责MultipartFile类型到byte[]]类型的转换;
- org.springframework.web.multipart.support.StringMultipartFileEditor:负责MultipartFile类型到String类型的转换。
2.Handler 与 HandlerAdaptor
1.处理流程
HandlerMapping会通过HandlerExecutionchain返回一个Handler(通常是Controller)用于具体Web请求的处理。
在Spring MVC中,任何可以用于Web请求处理的处理对象统称为Handler。Controller是Handler的一种特殊类型。
所以,一般意义上讲,任何类型的Handler都可以在Spring MVC中使用,比如Struts的Action和WebWork的Action等,只要它们是用于处理Web请求的处理对象就行。
那么,对于Dispatcherservlet来说,它如何来判断我们到底使用的是什么类型的Handler,又如何决定调用Handler对象的哪个方法来处理Web请求呢?
HandlerAdaptor:为了能够以统一的方式调用各种类型的Handler,DispatcherServlet将不同Handler的调用职责转交给了HandlerAdaptor。
HandlerAdaptor定义如下:
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
// 为返回给客户端的Last-Modified这个HTTP头提供相应的时间值。如果我们不想支持该功能,直接返回-1即可
long getLastModified(HttpServletRequest request, Object handler);
}
调用流程,DispatcherServlet调用多个Handleradaptor,来处理当前HandlerMapping所返回的Handler:
- Dispatcherservlet从HandlerMapping获得一个Handler之后,将询问HandlerAdaptor的supports(…)方法,以便了解当前HandlerAdaptor是否支持HandlerMapping刚刚返回的Handler类型的调用。
- 如果supports(…)返回true,DispatcherServlet则调用HandlerAdaptor的handle(…)方法,同时将刚才的Handler作为参数传入。
- 方法执行后将返回ModelAndview,之后的工作由viewResolver接手。
也就是说,当支持新的Handler 类型时,只需要为DispatcherServlet提供新的HandlerAdaptor实现即可。
2.Handler实现类
SpringMVC提供的Handler,除了Controller,还有ThrowawayController。
自定义Handler:
- 也可以实现自定义的
MyHandler
,不需要强制Hander实现任何接口,仅是一个简单的POJO对象即可。 - 然后再提供能识别该Handler的
HandlerMapping
实现。例如的基于注解的Handler,Spring 2.5就提供了特定的DefaultAnnotationHandlerMapping,处理新提供的基于注解的Handler的查找。 - 最后再提供一个
HandlerAdaptor
,让Dispatcherservlet调用,以处理我们的Handler即可。
3.HandlerAdaptor实现类
实现接口即可。
要能够给出对应的HandlerAdaptor实现,任何类型的Handler都可以纳入Spring MVC使用。
4.声明 Handler 与HandlerAdaptor
Handler 和 HandlerAdaptor 都有了,现在需要Dispatcherservlet知道它们的存在。
- 新建HandlerMapping:如果现有的HandlerMapping不足以”感知”到我们的Handler类型的话,那么我们需要提供一个能够”感知”我们Handler的HandlerMapping实现类
- 注册HandlerMapping:将HandlerMapping注册到DispatcherServlet的WebApplicationContext中即可
- 注册HandlerAdaptor:将HandlerAdaptor实现类添加到WebApplicationContext即可。可以添加多个。
默认的是BeanNameUrlHandlerMapping
,以及HttpRequestHandlerAdapter
等,用于匹配Controller。
3.HandlerInterceptor拦截器
1.处理流程
HandlerMapping返回的用于处理具体Web请求的Handler对象,是通过一个HandlerExecutionChain对象进行封装的。
HandlerExecutionChain就是一个数据载体,它包含了两方面的数据:
- Handler:用于处理Web请求的Handler。
- HanalerInterceptor:一组HandlerInterceptor,可以在Handler的执行前后对处理流程进行拦截操作。
HandlerInterceptor接口:
public interface HandlerInterceptor(
// handler是否可执行
boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object handler)throws Exception;
void postHandle(HttpServletRequest request,HttpServletResponse response, Object handler, ModelAndview modelAndview) throws Exception;
void afterCompletion(HttpServletRequest request,HEtpServletResponse response,Object handler,Exception ex)throws Excepion;
}
方法解析:
- preHandle(…):在HandlerAdaptor调用具体的Handler处理Web请求之前拦截。true:流程可往后走,后继HandlerIncerceptor的preHandle(..)将继续执行。全部通过后,handler可以执行。false:不允许后继流程的继续执行,后继HandlerInterceptor也不能执行。
- postHandle(…):在HandlerAdaptor调用具体的Handler处理完Web请求之后,解析和渲染视图之前拦截。此时我们可以获取到Handler执行后的结果,即ModelAndview。可以对ModelAndaview中的数据进行处理。不可阻断执行流程。
- afterCompletion(…):在框架内整个处理流程结束之后,即视图全部渲染完后,不管是否发生异常,该拦截方法都将被执行。异常结束,可以拿到异常。另外还可以清理相应的资源。不可阻断执行流程。
可以再复习下前面的图:
2.HandlerInterceptor实现类
- org.apringframework.web.servlet.handler.UserRoleAuthorizationInterceptor:允许我们通过HttpServletRequest的isUserInRole(..)方法,使用指定的一组用户角色(UserRoles)对当前请求进行验证。不通过则返回403或特定页面。
- org.springframework.web.servlet.mvc.WebContentInterceptor:内容检查。
- 检查请求方法类型是否在支持方法之列。
- 查必要的Session实例。
- 查缓存时间并通过设置相应HTTP头(Header)的方式控制缓存行为。
3.HandlerInterceptor自定义实现
实现思路:
- 实现自定义拦截器:继承
HandlerInterceptorAdapt
,实现相应的拦截方法即可。 - 注册至webApplicationContext:最后将其添加至webApplicationContext即可。
- 注册至HandlerMapping:HandlerInterceptor -》 HandlerExecutionChain -》HandlerMapping:,AbstractHandlerMapping作为几乎所有HandlerMapping实现类的父类,提供了setInterceptors(…)方法以接受一组指定的HandlerInterceptor实例。所以,要使我们的HandlerInterceptor发挥作用,只要将它添加到相应的HandlerMapping即可。
4.HandlerInterceptor和Filter的区别
HandlerInterceptor和Servlet的Filter的区别:
- Filter:Servlet标准组件,生命周期由Web管理。位于DispatcherServlet前。粒度比较粗。
- HandlerInterceptor:SpringMVC组件,需要在web.xml中配置,生命周期由Spring管理。位于DispatcherServlet内部,对Handler的执行进行拦截。粒度很细,可以在Handler的pre、post、after进行拦截。
两者关系如下:
4.HandlerExceptionResolver异常处理
HandlerExceptionResolver对异常的处理范围仅限于Handler查找以及Handler执行期间,也就是图25-2中矩形所圈定的范围。
1.处理流程
处理流程:
- 如果Handler执行过程中没有任何异常,将以ModelAndaview的形式返回后继流程要用的视图和模型型数据信息,
- 一旦出现异常情况HandlerExceptionResolver将接手异常情况的处理,处理完成后,将同样以ModelAndview的形式返回后继处理流程要使用的视图和模型数据信息。
- DispatcherServlet拿到ModelAndaview后,继续调用viewResolver和lview对这些返回的信息进行处理。
HandlerExceptionResolver定义如下:
public interface HandlerExceptionResolver{
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
2.HandlerExceptionResolver实现类
框架只提供了org.springframework.web.servlet.handler.SimpleMappingExceptionResolver这一个可用的实现类。不过通常情况下也够用了。
SimpleMappingExceptionResolver使用Properties来管理具体异常类型与所要转向的错误页面之间的映射对应关系,映射保存在exceptionMappings属性。
SimpleMappingExceptionResolver还定义了其他几个属性,以进一步定制其功能,主要为如下几个:
- defaultErrorview:指定一个默认的错误信息页面对应的逻辑视图名。当无法通过exceptionMappings查找到可用的视图名的时候,返回该视图。
- defaultstatusCode:异常情况下默认返回给客户端的HTTP状态码。
- exceptionAttribute:在错误信息页面中对抛出的异常进行访问。
- mappedHandlers和mappedHandlerClasses:只指定对某几个Handler或者某几种Handler类型所抛出的异常进行处理。
5.LocalResolver国际化视图
1.处理流程
在ViewResolver根据逻辑视图名解析视图的时候,ViewResolver
的resolveviewName(viewName, locale)
方法除了接受要解析的逻辑视图名作为参数之外,还同时接受一个Locale类型对象。这样,ViewResolver就可以根据Locale的不同而返回针对不同Locale的视图实例。
那么,viewResolver所接受的Locale实例是从何而来的呢?如何获取用户所对应的Locale呢?
LocalResolver
:用于封装Locale获取和解析方式。前面说可以有多种方式获取用户通过浏览器提交的Web请求所对应的Locale值,比如,根据HTTP的Accept-Language协议头进行解析,或者读取用户浏览器端存储的相应Cookie值等。由于有很多处理方式,Spring MVC用org.springframework.web.servlet.LocaleResolver接口来对各种可能的Locale值的获取/解析方式进行统一的策略抽象。该接口定义如下∶
public interface LocaleResolver{
// 根据当前Locale解析策略获取当前请求对应的Locale值
Locale resolveLocale(HttpServletRequest request);
// 如果当前策略支持Locale的更改,那么可以该方法对当前策略默认取得的Locale值进行变更。
void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale);
}
2.LocalResolver实现类
主要有以下:
- FixedLocaleResolver。最简单的LocaleResolver实现类。
- AcceptHeaderLocaleResolver:据Accept-Language协议头来解析并返回当前Web请求对应的Locale值。
- SessionLocaleResolver:据指定键值从Session中获取相应的Locale。
- CookieLocaleResolver:客户端浏览器没有禁止使用Cookie的话,也可以使用Cookie来管理Locale信息。
要在Spring MVC应用中使用相应的LocaleResolver对Locale进行解析和设置,只需要将相应实现类添加到DispatherServlet的webApplicationContext中即可。
3.LocalechangerInterceptor切换Local
即在页面上切换不同的local,略。
6.ThemeResolve主题
SpringMVC框架提供了对Web应用程序所需要的主题功能的支持。
1.ThemeSource提供主题资源
通常,Web应用程序的主题是由一些能够影响整体应用显示的静态资源组成的,比如固定位置的背景图片、能够影响页面显示风格的CSS(层叠样式表)文件等。在Spring MVC中,org.springframework.ui.context.ThemeSource负责管理针对各个主题的那些静态资源,该接口定义如下∶
public interface ThemeSource{
Theme getTheme(String themeName);
}
处理流程:
- DispatcherServlet会在处理Web请求之前获取可用的Themesource实例,以便能够根据客户端的请求返回相应的主题资源。
- 实际上Dispatcherservlet获取的
ThemeSource
实例就是它自身所使用的webApplicationContext
。因为webApplicationContext本身就是一个ThemeSource(它自己实现了ThemeSource接口)。 - 虽然WebApplicationContext身为ThemeSource,但当有主题相关的请求需要处理的时候,它们都是将工作委派给某个
ThemeSource
的具体实现类,比如org.springframework.ui.context.support.ResourceBundleThemeSource。
2.ThemeResolver管理主题
通过指定主题名称,我们就能够从DispatcherServlet所使用的ThemeSource那里获取主题对应的各项资源,然后视图就能够根据这些主题资源来定制视图显示。
那么,怎么知道用户当前所选择的主题名称呢?
ThemeResolver:为了获取并管理用户的Locale信息,Spring MVC提供了LocaleResolver。与此类似,为了获取并管理用户所选择的主题,Spring MVC提供了ThemeResolver。org.springframework.web.servlet.ThemeResolver的主要工作就是解析并获取对应当前请求的主题是什么。
定义如下:
public interface ThemeResolver{
// 获取用户主题名称
String resolveThemeName(HttpServletRequest request);
// 根据这个主题的名称到ThemesSource那里获取相应资源进行显示
void seteThemeName(HttpServletRequest request, HttpServletResponse response, String themeName);
}
除了不能通过Http头,ThemeResolver像LocaleResolver一样使用其他三种策略来获取并且管理用户的主题:
- org.springframework.web.servlet.theme.PixedThemeResolver:默认使用。
- org.springframework.web.servlet.theme.SessionThemeResolve:从HttpSession中获取。
- org.springframework.web.servlet.theme.CookieThemeResolver:从cookie中获取。
只需将以上任一ThemeResolver实现添加到DispatcherServlet的webApplicationContext中,即可使用。
3.ThemeChangeIntercepto切换Theme
即在页面上切换不同的Theme,略。
小结
提问
细说SpringMVC处理流程?文件上传、Handler适配器、Handler拦截器、Handler异常处理、国际化、主题…
要想增加新的Handler,怎么实现?
拦截器可以在哪些地方拦截?如何实现一个自定义的拦截器?
拦截器和过滤器有什么区别?如何配置过滤器?
第26章 SpringMVC中基于注解的Controller
1.自己实现@Controller功能
基于注解的Controller,本质也就是一个自定义的Handler。下面我们来尝试一下,怎么实现它。
Handler
我们已经有了,就是加了@Controller注解的POJO。
我们需要解决两个问题:
- 访问哪个类:如何让Spring MVC框架类(其实就是Dispatcherservlet)知道当前Web请求应该由哪个基于注解的Controller处理?
- 访问哪个方法:如何让Spring MVC框架类知道调用基于注解的Controller的哪个方法来处理具体的Web请求?
解决方案:
- 提供HandlerMapping:用来处理Web请求到Handler的映射关系,
- 提供HandlerAdaptor:用来调用并执行自定义handler的处理逻辑。
1.自定义HandlerMapping
遍历所有可用的基于注解的Controller实现类,比对请求的路径信息与controller的注解中的路径信息,匹配时返回当前controller实现类即可。
代码如下:
public class AnnotationBasedHandlerMapping implements HandlerMapping {
private List<HandlerInterceptor>handlerInterceptors;
@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
HandlerExecutionChain chain = null;
Object[] AnnotationControllers = this.getAvailableAnnotationControllers();
for (Object Annotation Controller:AnnotationControllers) {
Class<?> clazz = AnnotationController.getClass();
if(clazz.isAnnotationPresent(RequestMapping.class)) {
RequestMapping mapping = clazz.getAnnotation(RequestMapping.class);
if(this.matches(mapping, request)) {
chain = new HandlerExecutionChain(AnnotationController);
if(!CollectionUtils.isEmpty(getHandlerInterceptors()))
chain.addInterceptors(getHandlerInterceptors().toArray(new HandlerInterceptor[getHandlerInterceptors().size()]));
break;
}
}
}
return chain;
}
// 用于获取所有基于注解的Controller实现类
protected Object[] getAvailableAnnotationControllers(){}
// 获取RequestMapping所包含的信息,然后与当前请求进行对比的过程,
protected boolean matches(RequestMapping mapping, HttpServletRequest request){}
public List<HandlerInterceptor> getHandlerInterceptors(){
return handlerInterceptors;
}
public void setHandlerInterceptors(List<HandlerInterceptor> handlerInterceptors) {
this.handlerInterceptors = handlerInterceptors;
}
}
Spring 2.5中基于注解的Controller依赖于官方的HandlerMapping实现,即org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
。
DefaultAnnotationHandlerMapping
在实现原理上与上述AnnotationBasedHandlerMapping原型相似。它会扫描应用程序的Classpath,通过反射获取所有标注了@Controller的对象。
扫描Classpath然后获取标注了@Controller的对象这样的工作,由<concext∶component-scan/>来完成。
2.自定义HandlerAdaptor
HandlerMapping返回了处理Web请求的某个基于注解的Controller实例。HandlerAdaptor则告诉DispatcherServlet怎么使用这个实例。
实现思路:和前面的HandlerMapping类似,只需通过反射查找标注了@RequestMapping的方法定义,然后通过反射调用该方法,并返回Dispatcherservlet所需要的ModelAnaview即可。
实现代码:
public class AnnotationControllerHandlerAdaptor implements HandlerAdapter {
public boolean supports(Object handler) {
Class<?> clazz =handler.getClass();
return clazz.isAnnotationPresent(Controller.class) || clazz.isAnnotationPresent(RequestMapping.class);
}
public ModelAndview handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Method[]methods = handler.getClass().getDeclaredMethods();
for(Method method;methods) {
if (method.isAnnotationPresent(RequestMapping.class)) {
ModelAndvView mav = invokeAndReturn(method,handler,request);
return mav;
}
}
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return null;
}
// 调用方法
private ModelAndview invokeAndReturn(Method method, Object handler, HttpServletRequest request){
// 1.使用DataBinder或者其他装备将request参数绑定到方法参数
Object[] parameterValues = bind(request,method);
// 2.使用绑定后获得的相应参数调用方法
Object returnValue = method.invoke(handler,parameterValues);
ModelAndView mav = new ModelAndView();
if(returmValue instanceof String) {
mav.setViewName((String)returnValue);
} else if(returnValue instanceof ModelMap) {
mav.addA1lObjects(ModelMap) returnValue);
} else
...;
return mav;
}
public long getLastModified(HttpServletRequest arg0,Object arg1){
return -1;
}
}
上述是简化版本。生产中,还需要考虑一些问题:
- 判断是否调用该方法:如,RequestMapping指定了method = (RequestMethod.POST),那么Get请求肯定不能调用。
- 数据绑定如何映射参数名:因为反射只能获取参数类型,无法获取参数名。Spring 2.5是使用了ObjectWeb的ASM类库来获取方法参数名。
- 方法参数注解处理:例如
(@RequestParam("author") String name
。此时我们就需要加入更多的判断逻辑。 - 处理HttpSession:基于注解的Controller不依赖于任何ServletAPI,但是如果我们需要访问Httpsession怎么办呢?
- 返回值处理:返回值有没有类型限制呢?在Spring25的基于注解的Controller中,处理方法的返回值类型只能有规定的几种。
- 模型数据处理:假如需要返回模型数据,是让具体的处理方法通过返回值将模型参数返回,然后由HandlerAdaptor添加到要返回给DispatcherServlet使用的ModelAndview中,还是由HandlerAdaptor实现为基于注解的Controller传递一个能够进行模型数据访问的对象引用?这也需要考虑
Spring 2.5中为基于注解的Controller提供的HandlerAdaptor实现类是org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
。该类为我们考虑并实现了以上提到的几乎所有问题。DispatcherServlet将在初始化的时候就实例化了一个AnnotationMethodHandlerAdapter,用于支持基于注解的Controller。
2.基于注解的Controller详解
1.数据绑定:方法入参为特定类型
主要内容:
@Controller:标明基于注解的Controller类型Handler。
@RequeatMapping:标明访问路径。用于支持HandlerAdapor定位方法。参数示例:params=”locale=zh”。方法示例:method={RequestMethod.POST}
框架内部(或者说AnnotationMethodHandlerAdapter)所管理的对象引用:
quest/response/session:直接在方法参数中声明即可:
@RequestMapping(..) public void processMethod(HttpServletRequest request, ..){...}
WebRequest,和前面三个类型
java.io.InputStream / java.io.Reader:调用request.getInputStream() 或 request.getReader()获得
java.io.OutputStream / java.io.Writer:调用response.getOutputStream() 或 response.getwriter()获得
java.util.Map/org.apringframework.util.ModelMap:直接声明方法参数即可:
@RequestMapping(..) public void processMethod(ModelMap model){ //添加模型数据 model.addAttribute("commandobject",.); //根据Key获取相应模型数据 Object cmd= model.get("command"); } // 具体实现 ModelAndView mav =...; ModelMap modelMap = new ModelMap(); //调用 processMethod(modelMap); //将模型数据添加到ModelAndView mav.addAllObjects(modelMap); return mav;
使用@RequestParam或者eModelAttribute所标注的方法参数:后续解析。
org.Bpringframework.validation.Errors/org.Bpringframework.validation.BindingReult:用于对Command对象进行数据验证的Errors或者BindingResult对象。
org.apringframework.web.bind.support.SessionStatus:用于管理请求处理之后Session的状态,比如清除Session中的指定数据。
基于注解的Controller的请求处理方法返回值类型可以有如下4种形式:
org.springframework.web.servlet.ModelAndview:视图信息和模型信息都能通过它返回。
java.lang.String。代表逻辑视图名。模型数据需要以其他形式提供,比如为处理方法声明一个ModelMap类型的参数。
org.springframework.ui.ModelMap。ModelMap类型返回值只包含了模型数据信息而没有视图信息,框架类将根据请求的路径,按照默认规则提取相应的逻辑视图名来使用,比如∶
// simple将被作为逻辑视图名而使用。 @RequestMapping("/simple.anno") public ModelMap processMethod() {}
void。没有任何返回值,视图信息需要从请求路径中提取默认值,模型数据需要通过其他形式提供。
2.数据绑定:方法入参不是特定类型
方法入参是特定类型时,按上一小节方式处理,框架类将为这些特殊类型的方法参数提供框架内管理的相应对象的引用。
方法入参不是特定类型,按本小节方式进行绑定,根据某些规则把请求参数绑定到这些方法参数上。
Spring框架数据绑定时,默认采用JavaBean规范的PropertyEditor机制进行数据转换。
绑定逻辑如下:
默认绑定行为:根据名称匹配原则进行数据绑定。
@RequestParam指定:可以指定名称,以及是否必填。
public String processMethodone(eRequestParam(value="age",requiredsfalse) int authorAge){...}
添加自定义数据绑定规则:
使用@InitBinder标注的初始化方法:框架类在数据绑定之前,将保证该标注了eInitBinder的初始化方法被调用。
@Controller @RequestMapping("/reportSetting.anno") public class ReportSettingAnnotationController { @InitBinder public void customizeDataBinder(WebDataBinder dataBinder){ PropertyEditor propEditor = ..; dataBinder.registerCustomEditor(SomeDataType.class,propBditor); } ④RequestMapping(method=RequestMethod.GET) public String displayReportSettings(..) {...} @RequestMapping(method=RequestMethod.POST) public String updateReportSetting(..) {...} }
指定自定义的webBindingInitializer:如果某些Controller可以共享相同的一段定制逻辑,比如,几个Controller实现类都用到了某一特殊数据类型的绑定,那么它们就可能使用同一PropertyEditor实例。此时可以用webBindingInitializer统一实现。
// 此时需要把它添加到Dispatcherservlet的WebApplicationContext中。 public class GenericBindingInitializer implements webBindingInitializer{ public void initBinder(WebDataBinder binder,WebRequest request){ PropertyEditor propEditorOne =..; binder.registerCustomEditor(SomeDataType.class,propEditorOne); //如果需要,可以注册更多propertyEditor } }
3.使用@ModelAttribute 访问模型数据
为了能够访问模型数据,我们可以声明一个ModelMap类型的方法参数。
要为视图渲染提供更多模型数据,除了ModelMap类型的方法入参,还可以用@ModelAttribute。将@ModelAttribute标注在某个方法上,该方法所返回的数据将被添加到模型数据中。
使用示例如下:
@Controller
@RequestMapping("/reportSetting.anno")
public class ReportSettingAnnotationController{
public static final String FORMVIEW_NAME ="anno/reportSetting";
@Autowired
private IReportSettingManager reportSettingManager;
@ModelAttribute("command")
public Reportsettings referencepataLikehethod() {
ReportSettings reportSettings = getReportSettingManager().getReportsettingB();
return reportSettings;
}
@RequestMapping(method=RequestMethod.GET)
public String displayReportSettings(ModelMap model) {
return FORM_VIEW_NAME;
}
③RequestMapping (method=RequestMethod.POST)
public String updateReportSetting(..) {
· · ·
}
//对应reportSettingManager的getter和setter方法
}
4.通过@SessionAttribute管理 Session 数据
我们可以在声明一个HttpSession类型的入参来访问Session。
还可以通过@SesessionAttribute来访问。
用法示例:
@Controller
@RequestMapping("/reportSetting.anno")
@SessionAttributes("command")
public class ReportSettingAnnotationController {
public static final String FORM_VIEW_NAME ="anno/reportSetting";
@Autowired
private IReportSettingManager reportSettingManager;
// 将加到模型数据的Command对象存入Session
@RequestMapping(method-RequestMethod.GET)
public String displayReportSettings(ModelMap model) {
ReportSettings reportSettings = getReportSettingManager().getReportSettings();
model.addAttrlbute("command", reportSettings);
return FORM_VIEW_NAME;
}
// 获得前面存入session的command对象
@RequestMapping(method=RequestMethod.POST)
public String updateReportSetting(@ModelAttrlbute("command")Reportsettings reportSettings, BindingResult result, SessionStatus status){
if (result.hasErrors()) {
return FORM_VIEM_NAME;
}
getReportSettingManager().updateReportSettings(reportSetting8);
tatus.getComplete(); // 是将使用完的数据清除出Session。
return "redirect:reportSetting.anno";
}
//对应reportSettingManager的getter和seter方法定义
}
小结
提问
怎样实现基于注解的Controller功能?
SpringMVC用于处理注解的类?DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter。
请求参数如何进行数据绑定?特定类型…,非特定类型…
第27章 Spring MVC扩展
1.Spring MVC的 Convention Over Configuration:约定大于配置
Convention Over Configuration理念由Ruby On Rails这一Web开发框架引入。
1.Convention Over Configuration简介
“约定优先于配置”,从而减少配置量。
2.Spring MVC中的 Convention Over Configuration
1.Web请求与Handler之间的约定
Spring MVC提供了org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping
,用来根据请求的URL,到容器中选取约定名称或者约定类型的Controller实现类来进行当前Web请求的处理。
2.ModelAndaview中的约定
在相应的视图模板访问模型数据的时候,需要根据数据项的键(key)来获取对应的数据值。
引入Convention Over Configuration理念之后,即使我们不指定添加到ModelAndaview中的数据所对应的键,按照约定,ModelAndaview依然可以为添加的数据提供一个默认的键(key)来标志它。
3.Web请求与视图之间的约定
相应Controller处理完当前Web请求之后,会通过ModelAndaview返回后继流程要使用的逻辑视图名。但通常情况下,我们都是明确指定ModelAndview所返回的逻辑视图名是什么。
Spring MVC引入了org.springframework.web.servlet.RequestToviewNameTranslator
。当框架内部不能从ModelAndaview中获取可用的逻辑视图名的时候,由RequestToviewNamerranslator根据约定从当前请求的URL中提取。
2 Spring 3.0展望
- 引入自定义的ViewResolver:ContentNegotiatingViewResolver。
- @PathVarible:通过@Pathvarible标注的方法参数将被绑定请求映射中相应位置的值。
- 新的View:
JacksonJsonview
等。 - HiddenHittpMethodFilter:是一个Servlet的Filter实现,用于拦截并转换相应的请求方法。用来支持RESTFull。
ByTheWay:现在已经是5.0时代。
小结
提问
第7部分 Spring 框架对J2EE 服务的集成和支持(略)
(都是上古时期的技术了~
主要内容:
- 第28章 Spring框架内的JNDI支持:
- 第29章 Spring框架对JMS的集成:JMS相关
- 第30章 使用Spring发送E-mail:E-mail相关
- 第31章 Spring中的任务调度和线程池支持:Quartz、JDK Timer、TaskExecutor相关
- 第32章 Spring框架对J2EE服务的集成之扩展篇: MailMonit
- 第33章 Spring远程方案: Spring Remoting,基于RMI、HTTP、Web服务、或JMS。
tip:SpringIOC, AOP等的意义,可参照Mybatis揭秘。
转载请注明来源