💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 4.12 基于 Java 的容器配置 ### 4.12.1 基本概念:@Configuration 和@Bean Spring 中新的 Java 配置支持的核心就是@Configuration 注解的类。这些类主要包括 @Bean 注解的方法来为 Spring 的 IoC 容器管理的对象定义实例,配置和初始化逻辑。 使用@Configuration 来注解类表示类可以被 Spring 的 IoC 容器所使用,作为 bean 定义的资源。最简单的@Configuration 类可以是这样的: ``` @Configuration public class AppConfig { @Bean public MyService myService() { return new MyServiceImpl(); } } ``` 这和 Spring 的 XML 文件中的`<beans/>`非常类似,上面的 AppConfig 类和下面的代码是等同的: ``` <beans> <bean id="myService" class="com.acme.services.MyServiceImpl"/> </beans> ``` 正如你看到的,@Bean 注解扮演了和`<bean/>`元素相同的角色。@Bean 注解会在下面 的章节中详细地讨论。首先,我们要来看看使用基于 Java 的配置创建 Spring 容器的各种方 式。 ### 4.12.2 使用 AnnotationConfigApplicationContext 实例化 Spring 容器 下面的章节说明了 Spring 的 AnnotationConfigApplicationContext,在 Spring 3.0 中 是 新加 入 的 。这 个 全 能的 ApplicationContext 实 现 类 可 以 接 受不 仅 仅 是 @Configuration 类作为输入,也可以是普通的@Component 类,还有使用 JSR-330 元数 据注解的类。 当@Configuration 类作为输入时,@Configuration 类本身作为 bean 被注册了, 并且类内所有声明的@Bean 方法也被作为 bean 注册了。 当@Component 和 JSR-330 类 作为输 入时, 它们 被注册 为 bea n,并 且被 假设如 @Autowired 或@Inject 的 DI 元数据在类中需要的地方使用。 #### 4.12.2.1 简单构造 当实例化 ClassPathXmlApplicationContext 时,以大致相同的方式,当实例化 AnnotationConfigApplicationContext 时,@Configuration 类可能被作为输入。这 就允许在 Spring 容器中完全可以不使用 XML: ``` public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig. class); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); } ``` 正如上面所提到的,AnnotationConfigApplicationContext 不仅仅局限于和 @Configuration 类合作。任意@Component 或 JSR-330 注解的类都可以作为构造方法的输入。比如: ``` public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.MyService myService = ctx.getBean(MyService.class); myService.doStuff(); } ``` 上面假设 MyServiceImpl,Dependency1 和 Dependency2 使用了 Spring 依赖注 入注解,比如@Autowired。 #### 4.12.2.2 使用 `register(Class<?>...)`来编程构建容器 AnnotationConfigApplicationContext 可以使用无参构造方法来实例化,之后使 用 register() 方 法 来 配 置 。 这 个 方 法 当 编 程 构 建 AnnotationConfigApplicationContext 时尤其有用。 ``` public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AppConfig.class, OtherConfig.class); ctx.register(AdditionalConfig.class); ctx.refresh(); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); } ``` #### 4.12.2.3 使用 scan(String..)开启组件扫描 有经验的 Spring 用户肯定会熟悉下面这个 Spring 的 context:命名空间中的常用 XML 声明 ``` <beans> <context:component-scan base-package="com.acme"/> </beans> ``` 在上面的示例中,com.acme 包就会被扫描,去查找任意@Component 注解的类,那些 类就会被注册为 Spring 容器中的 bean。AnnotationConfigApplicationContext 暴露 出 scan(String ...)方法,允许相同的组件扫描功能: ``` public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.scan("com.acme"); ctx.refresh(); MyService myService = ctx.getBean(MyService.class); } ``` > ![](https://box.kancloud.cn/2016-01-25_56a58526bb420.gif) > 注意 > 记得@Configuration 类是使用@Component 进行元数据注释的,所以它们是组件扫 描的候选者!在上面的示例中,假设 AppConfig 是声明在 com.acme 包(或是其中的子 包)中的,那么会在调用 scan()方法时被找到,在调用 refresh()方法时,所有它的@Bean 方法就会被处理并注册为容器中的 bean。 #### 4.12.2.4 支 持 Web 应 用 的 AnnotationConfigWebApplicationContext WebApplicationContext 是 AnnotationConfigApplicationContext 的变种, 适用于 AnnotationConfigWebApplicationContext。当配置 Spring 的 Servlet 监听器 ContextLoaderListener,Spring MVC 的 DispatcherServlet 等时,这个实现类就 可能被用到了。下面的代码是在 web.xml 中的片段,配置了典型的 Spring MVC 的 Web 应用 程序。注意 contextClass 上下文参数和初始化参数的使用: ``` <web-app> <!-- 配置ContextLoaderListener使用 AnnotationConfigWebApplicationContext来代替默认的 XmlWebApplicationContext --> <context-param> <param-name>contextClass</param-name> <param-value> org.springframework.web.context.support.AnnotationCon figWebApplicationContext </param-value> </context-param> <!-- 配置位置必须包含一个或多个逗号或空格分隔的完全限定 @Configuration类。完全限定包也可以指定于组件扫描 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>com.acme.AppConfig</param-value> </context-param> <!-- 普通方式启动根应用上下文的ContextLoaderListener --> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <!-- 普通方式声明 Spring MVC 的 DispatcherServlet --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <!-- 配置DispatcherServlet使用 AnnotationConfigWebApplicationContext来代替默认的 XmlWebApplicationContext --> <init-param> <param-name>contextClass</param-name> <param-value> org.springframework.web.context.support.Annotation ConfigWebApplicationContext </param-value> </init-param> <!--配置位置必须包含一个或多个逗号或空格分隔的完全限定 @Configuration类--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>com.acme.web.MvcConfig</param-value> </init-param> </servlet> <!-- 映射所有的请求到/main/*中来派发servlet --> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/main/*</url-pattern> </servlet-mapping> </web-app> ``` ### 4.12.3 构成基于 Java 的配置 #### 4.12.3.1 使用@Import 注解 就像 Spring 的 XML 文件中使用的`<import/>`元素帮助模块化配置一样,@Import 注 解允许从其它配置类中加载@Bean 的配置: ``` @Configuration public class ConfigA { public @Bean A a() { return new A(); } } @Configuration @Import(ConfigA.class) public class ConfigB { public @Bean B b() { return new B(); } } ``` 现在,当实例化上下文时,不需要指定 ConfigA.class 和 ConfigB.class 了,仅 仅 ConfigB 需要被显式提供: ``` public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class); // 现在bean A 和bean B都会是可用的... A a = ctx.getBean(A.class); B b = ctx.getBean(B.class); } ``` 这种方式简化了容器的实例化,仅仅是一个类需要被处理,而不是需要开发人员在构造 时记住很多大量的@Configuration 类。 在引入的@Bean 定义中注入依赖 上面的示例是可行的,但是很简单。在很多实际场景中,bean 会有依赖其它配置的类的依赖。当使用 XML 时,这本身不是什么问题,因为没有调用编译器,而且我们可以仅仅 声明 ref="someBean" 并且相信 Spring 在 容 器 初 始化 时 可以 完 成。 当 然 ,当 使 用 @Configuration 的类时,Java 编译器在配置模型上放置约束,对其它 bean 的引用必须 是符合 Java 语法的。 幸运的是,解决这个问题分层简单。记得@Configuration 类最终是容器中的 bean- 这就是说它们可以像其它 bean 那样利用@Autowired 注入元数据! 我们来看一个更加真实的语义,有几个@Configuration 类,每个都依赖声明在其它类中的 bean: ``` @Configuration public class ServiceConfig { private @Autowired AccountRepository accountRepository; public @Bean TransferService transferService() { return new TransferServiceImpl(accountRepository); } } @Configuration public class RepositoryConfig { private @Autowired DataSource dataSource; public @Bean AccountRepository accountRepository() { return new JdbcAccountRepository(dataSource); } } @Configuration @Import({ServiceConfig.class, RepositoryConfig.class}) public class SystemTestConfig { public @Bean DataSource dataSource() { /* 返回新的数据源 */ } } public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); // 所有的装配都使用配置的类... TransferService transferService = ctx.getBean(TransferService.class); transferService.transfer(100.00, "A123", "C456"); } ``` 完全限定引入的 bean 便于导航 在上面的场景中,使用@Autowired 工作正常,提供所需的模块化,但是准确地决定 在哪儿声明自动装配的 bean 还是有些含糊。比如,作为开发者来看待 ServiceConfig, 你如何准确知道@Autowired AccountRepository 在哪里声明的?它没有显式地出现 在代码中,这可能很不错。要记得 [SpringSource Tool Suite](http://www.springsource.com/products/sts) 提供工具可以生成展示所有对象是 如何装配起来的图片-那可能就是你所需要的。而且,你的 Java IDE 可以很容器发现所有的声明,还有使用的 AccountRepository 类型,也会很快地给你展示出@Bean 方法的位置 和返回的类型。 在这种歧义不被接受和你想有直接从 IDE 中从一个@Configuration 类到另一个导航 的情景中,要考虑自动装配配置类的本身: ``` @Configuration public class ServiceConfig { private @Autowired RepositoryConfig repositoryConfig; public @Bean TransferService transferService() { // 通过配置类到@Bean方法的导航! return new TransferServiceImpl(repositoryConfig.accountRepository()); } } ``` 在上面的情形中,定义 AccountRepository 是完全明确的。而 ServiceConfig 却紧紧耦合到 RepositoryConfig 中了;这就需要我们来权衡了。这种紧耦合可以使用 基于接口或抽象基类的@Configuration 类来减轻。考虑下面的代码: ``` @Configuration public class ServiceConfig { private @Autowired RepositoryConfig repositoryConfig; public @Bean TransferService transferService() { return new TransferServiceImpl(repositoryConfig.accountRepository()); } } @Configuration public interface RepositoryConfig { @Bean AccountRepository accountRepository(); } @Configuration public class DefaultRepositoryConfig implements RepositoryConfig { public @Bean AccountRepository accountRepository() { return new JdbcAccountRepository(...); } } @Configuration @Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // 导入具体的配置! public class SystemTestConfig { public @Bean DataSource dataSource() { /* 返回数据源 */ } } public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); TransferService transferService = ctx.getBean(TransferService.class); transferService.transfer(100.00, "A123", "C456"); } ``` 现在 ServiceConfig 和 DefaultRepositoryConfig 的耦合就比较松了,并且内 建的 IDE 工具也一直有效:对于开发人员来说会更加简单地获取 RepositoryConfig 实现 类的类型层次。以这种方式,导航@Configuration 类和它们的依赖就和普通的基于接口 代码的导航过程没有任何区别了。 #### 4.12.3.2 结合 Java 和 XML 配置 Spring 的@Configuration 类并不是完全 100%地支持 Spring XML 替换的。一些基本特 性,比如 Spring XML 的命名空间会保持在一个理想的方式下去配置容器。在 XML 便于使用 或是必须要使用的情况下,你也有另外一个选择:以“XML 为中心”的方式来实例化容器, 比如,ClassPathXmlApplicationContext,或者以“ Java 为中心”的方式,使用 AnnotationConfigurationApplicationContext 和@ImportResource 注解来引 入需要的 XML。 以“XML 为中心”使用@Configuration 类从一种特定的方式的包含@Configuration 类的 XML 文件启动 Spring 容器是不错的。 比如,在使用了 Spring XML 的大型的代码库中,根据需要并从已有的 XML 文件中创建 @Configuration 类是很简单的。在下面,你会发现在这种“XML 为中心”情形下,使用 @Configuration 类的选择。 以普通的 Spring `<bean/>`元素声明@Configuration 类 要记得@Configuration 的类最终仅仅是容器中的 bean。在这个示例中,我们创建了 名为 AppConfig 的@Configuration 类,并且将它包含在 system-test-config.xml 文件中作为`<bean/>`的定义。因为开启了`<context:annotation-config/>`配置,容器 会识别@Configuration 注解,以合适的方式处理声明在 AppConfig 中@Bean 方法。 ``` @Configuration public class AppConfig { private @Autowired DataSource dataSource; public @Bean AccountRepository accountRepository() { return new JdbcAccountRepository(dataSource); } public @Bean TransferService transferService() { return new TransferService(accountRepository()); } } system-test-config.xml <beans> <!-- 开启处理注解功能,比如@Autowired和@Configuration --> <context:annotation-config/> <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/> <bean class="com.acme.AppConfig"/> <bean class="org.springframework.jdbc.datasource.DriverManagerData Source"> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> </beans> jdbc.properties jdbc.url=jdbc:hsqldb:hsql://localhost/xdb jdbc.username=sa jdbc.password= public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-t est-config.xml"); TransferService transferService = ctx.getBean(TransferService.class); // ... } ``` > ![](https://box.kancloud.cn/2016-01-25_56a58526bb420.gif) > 注意 > 在上面的 system-test-config.xml 文件中,AppConfig 的`<bean/>`定义没有声明 id 元素。这么做也是可以接受的,就不必让其它 bean 去引用它了。同时也就不可能从 容器中通过明确的名称来获取它了。同样地,DataSource bean,通过类型自动装配,那么明确的 bean id 就不严格要求了。 使用`<context:component-scan/>`来检索@Configuration 类 因为@Configuration 是使用@Component 来元数据注解的,被@Configuration 注解的类是自动作为组件扫描的候选者的。使用上面相同的语义,我们可以重新来定义 system-test-config.xml 文件来利用组件扫描的优点。注意这种情况下,我们不需要 明确地声明`<context:annotation-config/>`,因为`<context:component-scan/>` 开启了相同的功能。 ``` system-test-config.xml <beans> <!-- 选择并注册AppConfig作为bean --> <context:component-scan base-package="com.acme"/> <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/> <bean class="org.springframework.jdbc.datasource.DriverManagerData Source"> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> </beans> ``` 使用了@ImportResource 导入 XML 的@Configuration 类为中心在@Configuration 类作为配置容器主要机制的应用程序中,使用一些 XML 还是必要的。 在这些情况中,仅仅使用@ImportResource 来定义 XML 就可以了。这么来做就实现了“Java 为中心”的方式来配置容器并保持 XML 在最低限度。 ``` @Configuration @ImportResource("classpath:/com/acme/properties-config.xml") public class AppConfig { private @Value("${jdbc.url}") String url; private @Value("${jdbc.username}") String username; private @Value("${jdbc.password}") String password; public @Bean DataSource dataSource() { return new DriverManagerDataSource(url, username, password); } } properties-config.xml <beans> <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/> </beans> jdbc.properties jdbc.url=jdbc:hsqldb:hsql://localhost/xdb jdbc.username=sa jdbc.password= public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig. class); TransferService transferService = ctx.getBean(TransferService.class); // ... } ```