💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 4.8 容器扩展点 通常情况下,应用程序开发人员不需要编写 ApplicationContext 实现类的子类。 相反,Spring 的 IoC 容器可以通过插件的方式来扩展,就是实现特定的整合接口。下面的几 个章节会来说明这些整合接口。 ### 4.8.1 使用 BeanPostProcessor 来自定义 bean BeanPostProcessor 接口定义了你可以提供实现你自己的(或覆盖容器默认的)实 例逻辑,依赖解析逻辑等的回调方法。如果你想在 Spring 容器完成实例化,配置和初始化 bean 之后实现一些自定义逻辑,那么你可以使用一个或多个 BeanPostProcessor 实现类 的插件。 你可以配置多个 BeanPostProcessor 实例,而且你还可以通过设置 order 属性来 控制这些 BeanPostProcessor 执行的顺序。 仅当 BeanPostProcessor 实现了 Ordered 接口你才可以设置设个属性;如果你想编写你自己的 BeanPostProcessor,你 也应该考虑实现 Ordered 接 口 。 要 了 解 更 多 细 节 , 可 以 参 考 JavaDoc 文档中的 BeanPostProcessor 和 Ordered 接口。 > ![](https://box.kancloud.cn/2016-01-25_56a58526bb420.gif) > 注意 > BeanPostProcessor 操作 bean(对象)实例;那也就是说,Spring 的 IoC 容器 实例化 bean 的实例,之后 BeanPostProcessor 来做它们要做的事情。 BeanPostProcessor 的范围是对于每一个容器来说的。如果你使用了容器继承,那么这才有所关联。如果你在一个容器中定义了 BeanPostProcessor,那么它仅仅 会在那个容器中后处理 bean。换句话说,一个容器中定义的 bean 不会被另外一个容器 中定义的 BeanPostProcessor 来进行后处理,即便是两个容器都是相同继承链上的 一部分。 要修改真正的 bean 定义(也就是说,定义 bean 的蓝图),你可以使用 4.8.2 节,“使 用 BeanFactoryPostProcessor 来 自 定 义 配 置 元 数 据 ” 描 述 的 BeanFactoryPostProcessor 来进行。 org.springframework.beans.factory.config.BeanPostProcessor 接口由两个回调方法构成。如果在容器中有一个类注册为后处理器,对于容器创建的每个 bean 的实例,后处理器从容器中获得回调方法,在容器初始化方法之前(比如 InitializingBean 的 afterPropertiesSet()方法和任意声明为初始化的方法)被调用,还有在 bean 初始化回调之后 被调用。后处理器可以对 bean 实例采取任何动作,包括完整忽略回调。通常来说 bean 的后处理器会对回调接口进行检查,或者会使用代理包装 bean。一些 Spring 的 AOP 基类也会 作为 bean 的后处理器实现来提供代理包装逻辑。 ApplicationContext 会自动检测任意实现了 BeanPostProcessor 接口的 bean 定义的配置元数据。ApplicationContext 注册这些 bean 作为后处理器,那么它们可以 在 bean 创建之后被调用。Bean 的后处理器可以在容器中部署,就像其它 bean 那样。 BeanPostProcessor 和 AOP 自动代理 实现了 BeanPostProcessor 接口的类是特殊的,会被容器不同对待。所有它们参照的 BeanPostProcessor 和 bea n 会在启动时被实例化,作为 ApplicationContext 启 动 阶 段 特殊 的 一 部 分 。 接 下 来 , 所 有 的 BeanPostProcessor 以排序的方式注册并应用于容器中的其它 bean。因为 AOP 自动 代理作为 BeanPostProcessor 本身的实现,它们为自动代理资格的直接引用的既不是 BeanPostProcessor 也不是 bean,因此没有织入它们的方面。 对于这样的 bean,你应该看到一个信息级的日志消息:“Bean foo 没有由所有BeanPostProcessor 接口处理的资格(比如:没有自动代理的资格)”。 下面的示例展示了如何在 ApplicationContext 中编写,注册和使用BeanPostProcessor。 #### 4.8.1.1 示例:BeanPostProcessor 风格的 Hello World 第一个示例说明了基本的用法。示例展示了一个自定义的 BeanPostProcessor 实现 类来调用每个由容器创建 bean 的 toString()方法并打印出字符串到系统的控制台中。 自定义 BeanPostProcessor 实现类的定义: ``` package scripting; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.BeansException; public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor { // 简单地返回实例化的bean public Object postProcessBeforeInitialization(Object bean, String beanName)throws BeansException { return bean; // 这里我们可能返回任意对象的引用... } public Object postProcessAfterInitialization(Object bean, String beanName)throws BeansException { System.out.println("Bean '" + beanName + "' created : " + bean.toString()); return 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-3.0.xsd http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-3.0.xsd"> <lang:groovy id="messenger" script-source="classpath:org/springframework/scripting/gr oovy/Messenger.groovy"> <lang:property name="message" value="Fiona Apple Is Just So Dreamy."/> </lang:groovy> <!-- 当上面的bean(messenger)被实例化时,这个自定义的BeanPostProcessor实 现会输出实际内容到系统控制台 --> <bean class="scripting.InstantiationTracingBeanPostProcessor" /> </beans> ``` 注意 InstantiationTracingBeanPostProcessor 仅仅是简单地定义。它也没有 命名,因为它会像其它 bean 那样被依赖注入。(前面的配置也可以定义成 Groovy 脚本支持 的 bean。Spring 2.0 动态语言支持在第 27 章中来详细说明) 下面示例的 Java 应用程序执行了前面配置的代码: ``` import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationConte xt; 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 ``` #### 4.8.1.2 示例:RequiredAnnotationBeanPostProcessor 配合自定义的 BeanPostProcessor 实现使用回调接口或者注解,是扩展 Spring IoC 容器的一种通用方式。Spring 的 RequiredAnnotationBeanPostProcessor 就是一个 例子 – 一种 BeanPostProcessor 实现,随着 Spring 一起发布,来保证 bean 中被(任意) 注解所标记的 JavaBean 属性在真正(配置)注入时有值。 ### 4.8.2 使用 BeanFactoryPostProcessor 自定义配置元数据 下 一 个 扩 展 点 我 们 要 来 看 看 org.springframework.beans.factory.config.BeanFactoryPostProcessor 。 这个接口的 语义和那 些 BeanPostProcessor 是相 似的, 但有一个 主要的不 同点: BeanFactoryPostProcessor 操作 bean 的配置元数据;也就是说 Spring 的 IoC 容器允许 BeanFactoryPostProcessor 来 读 取 配 置 元 数 据 并 在 容 器 实 例 化 BeanFactoryPostProcessor 以外的任何 bean 之前可以修改它。 你可以配置多个 BeanFactoryPostProcessor,并且你也可以通过 order 属性来控 制 这 些 BeanFactoryPostProcessor 执 行 的 顺 序 。 然 而 , 仅 当 BeanFactoryPostProcessor 实现 Ordered 接口时你才能设置这个属性。如果编写你自己的 BeanFactoryPostProcessor,你也应该考虑实现 Ordered 接口。参考 JavaDoc 文档来获取 BeanFactoryPostProcessor 和 Ordered 接口的更多细节。 > ![](https://box.kancloud.cn/2016-01-25_56a58526bb420.gif) > 注意 > 如果你想改变真实的 bean 实例(也就是说,从配置元数据中创建的对象),那么你 需要使用 BeanPostProcessor(在上面 4.8.1 节,“使用 BeanPostProcessor 来自定义 bean ”中描述)来代替。在 BeanFactoryPostProcessor (比如使用 BeanFactory.getBean())中来使用这些 bea n 的实例虽然在技术上是可行的,但 这么来做会引起 bean 过早实例化,违反标准的容器生命周期。这也会引发一些副作用, 比如绕过 bean 的后处理。 而且,BeanFactoryPostProcessor 的范围也是对每一个容器来说的。如果你 使 用 了容 器的 继承 的话 ,这 就是 唯一 相关 的点 了。 如果 你在 一个 容器 中定 义 了 BeanFactoryPostProcessor,那么它只会用于在那个容器中的 bean。一个容器中 Bean 的定义不会被另外一个容器中的 BeanFactoryPostProcessor 后处理,即便 两个容器都是相同继承关系的一部分。 当在 ApplicationContext 中声明时,bean 工厂后处理器会自动被执行,这就可以 对定义在容器中的配置元数据进行修改。Spring 包含了一些预定义的 bean 工厂后处理器,比如 PropertyOverrideConfigurer 和 PropertyPlaceholderConfigurer.。自定义的 BeanFactoryPostProcessor 也可以来用,比如,注册自定义的属性编辑器。 ApplicationContext 会 自 动 检 测 任 意 部 署 其 中 , 且 实 现 了 BeanFactoryPostProcessor 接口的 bean。在适当的时间,它用这些 bean 作为 bean 工厂后处理器。你可以部署这些后处理器 bean 作为你想用的任意其它的 bean。 > ![](https://box.kancloud.cn/2016-01-25_56a58526bb420.gif) > 注意 > 和 BeanPostProcessor 一样,通常你不会想配置 BeanFactoryPostProcessor 来进行延迟初始化。如果没有其它 bean 引用 Bean(Factory)PostProcessor,那么后 处理器就不会被初始化了。因此,标记它为延迟初始化就会被忽略,即便你在`<beans/>`元 素声明中设置 default-lazy-init 属性为 true,那么 Bean(Factory)PostProcessor 也会正常被初始化。 #### 4.8.2.1 示例:PropertyPlaceholderConfigurer 你可以使用来对使用了标准 Java Properties 格式的分离文件中定义的 bean 来声明属 性值。这么来做可以使得部署应用程序来自定义指定的环境属性,比如数据库的连接 URL 和密码,不会有修改容器的主 XML 定义文件或其它文件的复杂性和风险。 考虑一下下面这个基于 XML 的配置元数据代码片段,这里的 dataSource 就使用了占位符来定义。这个示例展示了从 Properties 文 件 中 配 置 属 性 的 方 法 。 在 运 行 时 , PropertyPlaceholderConfigurer 就会用于元数据并为数据源替换一些属性。指定替 换的值作为${属性-名}形式中的占位符,这里应用了 Ant/log4j/JSP EL 的风格。 ``` <bean class="org.springframework.beans.factory.config.PropertyPlaceho lderConfigurer"> <property name="locations" value="classpath:com/foo/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> ``` 而真正的值是来自于标准的 Java Properties 格式的文件: ``` jdbc.driverClassName=org.hsqldb.jdbcDriver jdbc.url=jdbc:hsqldb:hsql://production:9002 jdbc.username=sa jdbc.password=root ``` 因此,字符串${jdbc.username}在运行时会被值’sa’替换,对于其它占位符来说也是相同 的 , 匹 配 到 了 属 性 文 件 中 的 键 就 会 用 其 值 替 换 占 位 符 。 PropertyPlaceholderConfigurer 在很多 bean 定义的属性中检查占位符。此外,对 占位符可以自定义前缀和后缀。 使用 Spring 2.5 引入的 context 命名空间,也可以使用专用的配置元素来配置属性占 位符。在 location 属性中,可以提供一个或多个以逗号分隔的列表。 ``` <context:property-placeholder location="classpath:com/foo/jdbc.properties"/> ``` PropertyPlaceholderConfigurer 不仅仅查看在 Properties 文件中指定的属 性。默认情况下,如果它不能在指定的属性文件中发现属性,它也会检查 Java System 属性。 你可以通过设置 systemPropertiesMode 属性,使用下面整数的三者之一来自定义这种 行为: never(0):从不检查系统属性 fallback(1):如果没有在指定的属性文件中解析到属性,那么就检查系统属性。这是默 认的情况。 override(2):在检查指定的属性文件之前,首先去检查系统属性。这就允许系统属性覆 盖其它任意的属性资源。 查看 PropertyPlaceholderConfigurer 的 JavaDoc 文档来获取更多信息。 类名替换 你可以使用 PropertyPlaceholderConfigurer 来替换类名,在运行时,当你不得不去选择一个特定的实现类时,这是很有用的。比如: ``` <bean class="org.springframework.beans.factory.config.PropertyPlac eholderConfigurer"> <property name="locations"> <value>classpath:com/foo/strategy.properties< /value> </property> <property name="properties"> <value>custom.strategy.class=com.foo.DefaultStrategy< / value> </property> </bean> <bean id="serviceStrategy" class="${custom.strategy.class}"/> ``` 如果类在运行时不能解析成一个有效的类,那么在即将创建时,bean 的解析就失 败 了 , 这 是 ApplicationContext 在 对 非 延 迟 初 始 化 bean 的 preInstantiateSingletons()阶段发生的。 #### 4.8.2.2 示例:PropertyOverrideConfigurer PropertyOverrideConfigurer , 另 外 一 种 bean 工 厂 后 处 理 器 , 类 似 于 PropertyPlaceholderConfigurer,但不像后者,对于所有 bean 的属性,原始定义可 以有默认值或没有值。如果一个 Properties 覆盖文件没有特定 bean 的属性配置项,那么 就会使用默认的上下文定义。 注意,bean 定义是不知道被覆盖的,所以从 XML 定义文件中不能立即明显反应覆盖配 置。在多个 PropertyOverrideConfigurer 实例的情况下,为相同 bean 的属性定义不同的值,那么最后一个有效,这就是覆盖机制。 属性文件配置行像这种格式: ``` beanName.property=value ``` 比如: ``` dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql:mydb ``` 这个示例文件可以用于包含了 dataSource bean 的容器,它有 driver 和 url 属性。 复合属性名也是支持的,除了最终的属性被覆盖,只要路径中的每个组件都是非空的(假设由构造方法初始化)。在这个例子中... ``` foo.fred.bob.sammy=123 ``` ...foo bean 的 fred 属性的 bob 属性的 sammy 属性的值设置为标量 123。 注意指定的覆盖值通常是文字值;它们不会被翻译成 bean 的引用。当 XML 中的 bean 定义的原始值指定了 bean 引用时,这个约定也适用。 使用 Spring 2.5 引入的 context 命名空间,可以使用专用的配置元素来配置属性覆盖: ``` <context:property-override location="classpath:override.properties"/> ``` ### 4.8.3 使用 FactoryBean 来自定义实例化逻辑 实现了 org.springframework.beans.factory.FactoryBean 接口的对象它们 就是自己的工厂。 FactoryBean 接口就是 Spring IoC 容器实例化逻辑的可插拔点。如果你的初始化代码 很复杂,那么相对于(潜在地)大量详细的 XML 而言,最好是使用 Java 语言来表达。你可 以 创 建 自己 的 FactoryBean ,在类中编写复杂的初始化代码,之后将你自定义的 FactoryBean 插入到容器中。 FactoryBean 接口提供下面三个方法: Object getObject():返回工厂创建对象的实例。这个实例可能被共享,那就是看这个工厂返回的是单例还是原型实例了。 boolean isSingleton():如果 FactoryBean 返回单例的实例,那么该方法返回 true, 否则就返回 false。 Class getObjectType():返回由 getObject()方法返回的对象类型,或者事先不知 道类型时返回 null。 FactoryBean 的概念和接口被用于 Spring Framework 中的很多地方;随 Spring 发行, 有超过 50 个 FactoryBean 接口的实现类。 当你需要向容器请求一个真实的 FactoryBean 实例,而不是它生产的 bean,当调用 ApplicationContext 的 getBean()方法时,在 bean 的 id 之前要有连字符(&)。所以 对于一个给定 id 为 myBean 的 FactoryBean,调用容器的 getBean("myBean")方法返回的 FactoryBean 产品;而调用 getBean("&myBean")方法则返回 FactoryBean 实例本身。