参考:官方文档
1 容器扩展点
通常,开发者不需要去实现ApplicationContext
接口,Spring
提供了很多接口用于扩展容器的功能。
1.1 BeanPostProcessor接口
BeanPostProcessor
接口定义了回调方法,您可以实现这些方法来提供自己的(或覆盖容器的默认)实例化逻辑、依赖项解析逻辑,等等。如果您想在Spring
容器完成bean
的实例化、配置和初始化之后实现一些自定义逻辑,您可以插入一个或多个自定义的BeanPostProcessor
实现。
您可以配置多个BeanPostProcessor
实例,并且可以通过实现Ordered
接口来设置order
属性来控制这些BeanPostProcessor
实例执行的顺序。有关详细信息,请参见BeanPostProcessor
和 Ordered
接口的javadoc
。还可以请参见programmatic registration of BeanPostProcessor instances。
BeanPostProcessor
接口的定义如下:
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
org.springframework.beans.factory.config.BeanPostProcessor
接口定义了两个回调方法:
postProcessBeforeInitialization(Object bean, String beanName)
方法:对于该Spring IOC
容器所创建的每个bean
实例在初始化方法(如afterPropertiesSet()
和任意已声明的init
方法)调用前,将会调用BeanPostProcessor
中的postProcessBeforeInitialization()
方法 。postProcessAfterInitialization(Object bean, String beanName)
方法:在bean
实例初始化方法调用完成后,会调用BeanPostProcessor
中的postProcessAfterInitialization()
方法 。
1.2 例子
下面的例子展示了如何使用BeanPostProcessor
接口。
这个例子中,通过实现BeanPostProcessor
接口,在容器创建每个bean
的时候打印bean
的toString()
的内容到控制台。
package scripting;
import org.springframework.beans.factory.config.BeanPostProcessor;
/*
* 自定义类InstantiationTracingBeanPostProcessor实现BeanPostProcessor接口
*/
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
// 直接返回实例化的bean
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean; // 在这里其实可以返回任何对象
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 在bean初始化之后,打印toString()的内容到控制台
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
return bean;
}
}
下面是在xml
配置中注册这个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:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang.xsd">
<lang:groovy id="messenger"
script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
<lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
</lang:groovy>
<!-- 注册这个后处理器 -->
<bean class="scripting.InstantiationTracingBeanPostProcessor"/>
</beans>
下面是测试代码:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;
public final class Boot {
public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
Messenger messenger = (Messenger) ctx.getBean("messenger");
System.out.println(messenger);
}
}
控制台的输出为:
Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961
可以看到,后处理器在容器实例化messenger
这个bean
的时候,将toString()
的内容打赢到了控制台。
1.3 BeanFactoryPostProcessor接口
org.springframework.beans.factory.config.BeanFactoryPostProcessor
接口是Spring
提供的一个扩展点。Spring IoC
容器允许BeanFactoryPostProcessor
读取bean
的配置元数据,并在容器实例化除BeanFactoryPostProcessor
实例之外的任何bean
之前更改它的配置元数据信息。
同样的,你也可以配置多个BeanFactoryPostProcessor
实例,并且可以实现Ordered
接口来控制执行的顺序。
Spring
包含许多预定义的BeanFactoryPostProcessor
的实现,例如PropertyOverrideConfigurer
和PropertyPlaceholderConfigurer
。您还可以使用自定义BeanFactoryPostProcessor
,例如,注册自定义属性编辑器。
1.4 例子
PropertyPlaceholderConfigurer
下面的例子说明了如何使用PropertyPlaceholderConfigurer
。
你可以通过配置PropertyPlaceholderConfigurer
来从外部的.properties
文件中读取配置,来设置属性例如数据库url
、password
等。
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>
<bean id="dataSource" destroy-method="close"
class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
对应的配置文件:
jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root
在Spring 2.5
之后,可以使用<context:property-placeholder />
来配置PropertyPlaceholderConfigurer
:
<context:property-placeholder location="classpath:com/something/jdbc.properties"/>
PropertyOverrideConfigurer
下面的例子展示了如何使用PropertyOverrideConfigurer
。
与PropertyPlaceholderConfigurer
不同的是:PropertyOverrideConfigurer
利用属性文件的相关信息,覆盖XML
配置文件中定义。即PropertyOverrideConfigurer
允许XML
配置文件中有默认的配置信息。如果PropertyOverrideConfigurer
的属性文件有对应配置信息,则XML
文件中的配 置信息被覆盖;否则,直接使用XML
文件中的配置信息。
使用PropertyOverrideConfigurer
属性文件的格式如下:
beanName.property=value
beanName
是属性占位符企图覆盖的bean
名, property
是企图覆盖的属性名。
下面是配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置一个属性占位符bean ApplictionContext能自动识别PropertyPlaceholderConfigurer bean -->
<bean id="propertyOverrider" class= "org.springframework.beans.factory.config.PropertyOverrideConfigurer">
<property name="locations">
<list>
<value>dbconn.properties</value>
<!--如果有多个属性文件,依次在下面列出来 -->
</list>
</property>
</bean>
<!-- 配置本地的DBCP 数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" destroy-method="close">
<property name="driverClassName" value="dd"/>
<property name="url" value="xx"/>
<property name="username" value="dd"/>
<property name="password" value="dd"/>
</bean>
</beans>
容器自动注册propertyOverrider
这个bean
,读取dbconn.properties
文件中的属性,并用于覆盖目标bean
的属性。其配置文件中dataSource
这个bean
的属性完全是随意输入的,最终被属性文件的配置覆盖,其属性文件如下:
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/demo
dataSource.username=root
dataSource.password=1234567890
1.5 FactoryBean接口(非常重要)
FactoryBean
接口提供了功能让我们可以自定义一个bean
的实例化逻辑。
org.springframework.beans.factory.FactoryBean
是Spring
容器实例化bean
逻辑的扩展点。如果你有复杂的初始化代码,而不是冗长的XML
,那么你可以创建自己的FactoryBean
,在该类中编写复杂的初始化,然后将定制的FactoryBean
插入容器中。
FactoryBean
接口的定义如下:
public interface FactoryBean<T> {
@Nullable
T getObject() throws Exception;
@Nullable
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}
它定义了3个方法:
Object getObject()
:返回此工厂创建的对象的实例。可以共享实例,这取决于该工厂返回的是单例还是原型。Class getObjectType()
:返回getObject()
方法返回的对象类型,如果类型事先不知道,则返回null
。boolean isSingleton()
:如果FactoryBean
返回单例,则返回true
,否则返回alse
。
当你在容器中想要获取对应bean
的FactoryBean
的实例的时候,在名称前面先加上&
。例如,getBean("myBean")
返回的是一个叫做myBean
的bean
,而getBean("&myBean")
返回的是对应的FactoryBean
。
注意:
FactoryBean
和BeanFactory
很像,但是它们的作用是不一样的。
1.6 FactoryBean和BeanFactory的区别,以及FactoryBean的使用
1.6.1 FactoryBean和BeanFactory的区别
(一)BeanFacotry
BeanFactory
,以Factory
结尾,表示它是一个工厂类(接口), 它是负责生产和管理bean
的一个工厂。在Spring
中,BeanFactory
是IOC
容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。BeanFactory
只是个接口,并不是IOC容器的具体实现,但是Spring
容器给出了很多种实现,如 DefaultListableBeanFactory
、XmlBeanFactory
、ApplicationContext
等,其中XmlBeanFactory
就是常用的一个,该实现将以XML
方式描述组成应用的对象及对象间的依赖关系。
BeanFactory
和ApplicationContext
就是Spring
框架的两个IOC
容器,现在一般使用ApplicationnContext
,其不但包含了BeanFactory
的作用,同时还进行更多的扩展。
BeanFacotry
是Spring
中比较原始的Factory
。如XMLBeanFactory
就是一种典型的BeanFactory
。
原始的BeanFactory
无法支持Spring
的许多插件,如AOP功能、Web应用等。ApplicationContex
接口,它由BeanFactory
接口派生而来, ApplicationContext
包含BeanFactory
的所有功能,还扩展了很多功能,通常建议比BeanFactory
优先。
(二)FactoryBean
一般情况下,Spring
通过反射机制利用<bean>
的class
属性指定实现类实例化bean
。在某些情况下,实例化bean
过程比较复杂,如果按照传统的方式,则需要在<bean>
中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring
为此提供了一个org.springframework.bean.factory.FactoryBean
的工厂类接口,用户可以通过实现该接口定制实例化bean
的逻辑。
FactoryBean
接口对于Spring
框架来说占用重要的地位,Spring
自身就提供了70
多个FactoryBean
的实现。它们隐藏了实例化一些复杂Bean
的细节,给上层应用带来了便利。从Spring3.0
开始,FactoryBean
开始支持泛型,即接口声明改为FactoryBean<T>
的形式。
以Bean
结尾,表示它是一个Bean
。
不同于普通Bean
的是:它是实现了FactoryBean<T>
接口的Bean
,根据该Bean
的ID
从BeanFactory
中获取的实际上是FactoryBean
的getObject()
返回的对象,而不是FactoryBean
本身,如果要获取FactoryBean
对象,请在id
前面加一个&
符号来获取。
1.6.2 FactoryBean的使用
应用场景
FactoryBean
通常是用来创建比较复杂的bean
。一般的bean
直接用xml
配置即可,但如果一个bean
的创建过程中涉及到很多其他的bean
和复杂的逻辑,用xml
配置比较困难,这时可以考虑用FactoryBean
。
应用案例
很多开源项目在集成Spring
时都使用到FactoryBean
,比如 MyBatis3 提供 mybatis-spring项目中的 org.mybatis.spring.SqlSessionFactoryBean
:
<bean id="tradeSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="trade" />
<property name="mapperLocations" value="classpath*:mapper/trade/*Mapper.xml" />
<property name="configLocation" value="classpath:mybatis-config.xml" />
<property name="typeAliasesPackage" value="com.bytebeats.mybatis3.domain.trade" />
</bean>
org.mybatis.spring.SqlSessionFactoryBean
如下:
package org.mybatis.spring;
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
private static final Log LOGGER = LogFactory.getLog(SqlSessionFactoryBean.class);
// ......
}
另外,阿里开源的分布式服务框架 Dubbo 中的Consumer
也使用到了FactoryBean
:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd
">
<!-- 当前应用信息配置 -->
<dubbo:application name="demo-consumer" />
<!-- 暴露服务协议配置 -->
<dubbo:protocol name="dubbo" port="20813" />
<!-- 暴露服务配置 -->
<dubbo:reference id="demoService" interface="com.alibaba.dubbo.config.spring.api.DemoService" />
</beans>
<dubbo:reference
对应的Bean是com.alibaba.dubbo.config.spring.ReferenceBean
类,如下:
public class ReferenceBean<T> extends ReferenceConfig<T> implements FactoryBean, ApplicationContextAware, InitializingBean, DisposableBean {
}
实践
当前微服务日趋流行,项目开发中不可避免的需要去调用一些其他系统接口,这个时候选择一个合适的HTTP client library 就很关键了,本人项目开发中使用 Square 开源的OkHttp ,关于OkHttp 可以参考 OkHttp Recipes
OkHttp本身的API 已经非常好用了,结合Spring 以及在其他项目中复用的目的,对OkHttp 创建过程做了一些封装,例如超时时间、连接池大小、http代理等。
maven依赖:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.6.0</version>
</dependency>
首先,自定义一个OkHttpClientFactoryBean
,代码如下:
package com.bytebeats.codelab.http.okhttp;
import com.bytebeats.codelab.http.util.StringUtils;
import okhttp3.*;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.concurrent.TimeUnit;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2017-03-04 22:18
*/
package com.tao.springboot.demo;
import okhttp3.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import java.io.IOException;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.concurrent.TimeUnit;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2017-03-04 22:18
*/
public class OkHttpClientFactoryBean implements FactoryBean<OkHttpClient>, DisposableBean {
private int connectTimeout;
private int readTimeout;
private int writeTimeout;
/**
* http proxy config
**/
private String host;
private int port;
private String username;
private String password;
/**
* OkHttpClient instance
**/
private OkHttpClient client;
@Override
public OkHttpClient getObject() throws Exception {
ConnectionPool pool = new ConnectionPool(5, 10, TimeUnit.SECONDS);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.writeTimeout(writeTimeout, TimeUnit.MILLISECONDS)
.connectionPool(pool);
if (StringUtils.isNotBlank(host) && port > 0) {
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port));
builder.proxy(proxy);
}
if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
Authenticator proxyAuthenticator = new Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
String credential = Credentials.basic(username, password);
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
}
};
builder.proxyAuthenticator(proxyAuthenticator);
}
client = builder.build();
return client;
}
@Override
public Class<?> getObjectType() {
return OkHttpClient.class;
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public void destroy() throws Exception {
if (client != null) {
client.connectionPool().evictAll();
client.dispatcher().executorService().shutdown();
client.cache().close();
client = null;
}
}
public void setConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
}
public void setReadTimeout(int readTimeout) {
this.readTimeout = readTimeout;
}
public void setWriteTimeout(int writeTimeout) {
this.writeTimeout = writeTimeout;
}
public void setHost(String host) {
this.host = host;
}
public void setPort(int port) {
this.port = port;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
}
项目中引用 applicationContext.xml:
<?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:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="okHttpClient" class="com.bytebeats.codelab.http.okhttp.OkHttpClientFactoryBean">
<property name="connectTimeout" value="2000" />
<property name="readTimeout" value="2000" />
<property name="writeTimeout" value="2000" />
<property name="host" value="" />
<property name="port" value="0" />
<property name="username" value="" />
<property name="password" value="" />
</bean>
</beans>
写个测试类测试一下:
@Service
public class DemoService {
@Resource
private OkHttpClient client;
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://www.baidu.com/")
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
作者在上边的自定义
OkHttpClientFactoryBean
中,对okhttp
进行了封装。该类实现了
FactoryBean
接口。这样,在代码中注入
OkHttpClient
依赖的时候,得到的是OkHttpClientFactoryBean
中getObject()
方法返回的对象。注意返回的对象不是OkHttpClientFactoryBean
的实例。如果想得到
OkHttpClientFactoryBean
的实例,在制定name的时候,加上&
前缀,如下图: