二、夯实基础:JavaConfig与常见Annotation
2.1、JavaConfig
我们知道 bean是Spring IOC中非常核心的概念,Spring容器负责bean的生命周期的管理。在最初,Spring使用XML配置文件的方式来描述bean的定义以及相互间的依赖关系,但随着Spring的发展,越来越多的人对这种方式表示不满,因为Spring项目的所有业务类均以bean的形式配置在XML文件中,造成了大量的XML文件,使项目变得复杂且难以管理。
后来,基于纯Java Annotation依赖注入框架 Guice出世,其性能明显优于采用XML方式的Spring,甚至有部分人认为, Guice可以完全取代Spring( Guice仅是一个轻量级IOC框架,取代Spring还差的挺远)。正是这样的危机感,促使Spring及社区推出并持续完善了 JavaConfig子项目,它基于Java代码和Annotation注解来描述bean之间的依赖绑定关系。比如,下面是使用XML配置方式来描述bean的定义:
<bean id="bookService" class="cn.moondev.service.BookServiceImpl"></bean>
而基于JavaConfig的配置形式是这样的:
@Configuration public class MoonBookConfiguration { // 任何标志了@Bean的方法,其返回值将作为一个bean注册到Spring的IOC容器中 // 方法名默认成为该bean定义的id @Bean public BookService bookService() { return new BookServiceImpl(); } }
如果两个bean之间有依赖关系的话,在XML配置中应该是这样:
<bean id="bookService" class="cn.moondev.service.BookServiceImpl"> <property name="dependencyService" ref="dependencyService"/> </bean> <bean id="otherService" class="cn.moondev.service.OtherServiceImpl"> <property name="dependencyService" ref="dependencyService"/> </bean> <bean id="dependencyService" class="DependencyServiceImpl"/>
而在JavaConfig中则是这样:
@Configuration public class MoonBookConfiguration { // 如果一个bean依赖另一个bean,则直接调用对应JavaConfig类中依赖bean的创建方法即可 // 这里直接调用dependencyService() @Bean public BookService bookService() { return new BookServiceImpl(dependencyService()); } @Bean public OtherService otherService() { return new OtherServiceImpl(dependencyService()); } @Bean public DependencyService dependencyService() { return new DependencyServiceImpl(); } }
你可能注意到这个示例中,有两个bean都依赖于dependencyService,也就是说当初始化bookService时会调用 dependencyService(),在初始化otherService时也会调用 dependencyService(),那么问题来了?这时候IOC容器中是有一个dependencyService实例还是两个?这个问题留着大家思考吧,这里不再赘述。
2.2、@ComponentScan
@ComponentScan注解对应XML配置形式中的 <context:component-scan>元素,表示启用组件扫描,Spring会自动扫描所有通过注解配置的bean,然后将其注册到IOC容器中。我们可以通过 basePackages等属性来指定 @ComponentScan自动扫描的范围,如果不指定,默认从声明 @ComponentScan所在类的 package进行扫描。正因为如此,SpringBoot的启动类都默认在 src/main/java下。
2.3、@Import
@Import注解用于导入配置类,举个简单的例子:
@Configuration public class MoonBookConfiguration { @Bean public BookService bookService() { return new BookServiceImpl(); } }
现在有另外一个配置类,比如: MoonUserConfiguration,这个配置类中有一个bean依赖于 MoonBookConfiguration中的bookService,如何将这两个bean组合在一起?借助 @Import即可:
@Configuration // 可以同时导入多个配置类,比如:@Import({A.class,B.class}) @Import(MoonBookConfiguration.class) public class MoonUserConfiguration { @Bean public UserService userService(BookService bookService) { return new BookServiceImpl(bookService); } }
需要注意的是,在4.2之前, @Import注解只支持导入配置类,但是在4.2之后,它支持导入普通类,并将这个类作为一个bean的定义注册到IOC容器中。
2.4、@Conditional
@Conditional注解表示在满足某种条件后才初始化一个bean或者启用某些配置。它一般用在由 @Component、 @Service、 @Configuration等注解标识的类上面,或者由 @Bean标记的方法上。如果一个 @Configuration类标记了 @Conditional,则该类中所有标识了 @Bean的方法和 @Import注解导入的相关类将遵从这些条件。
在Spring里可以很方便的编写你自己的条件类,所要做的就是实现 Condition接口,并覆盖它的 matches()方法。举个例子,下面的简单条件类表示只有在 Classpath里存在 JdbcTemplate类时才生效:
public class JdbcTemplateCondition implements Condition { @Override public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { try { conditionContext.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate"); return true; } catch (ClassNotFoundException e) { e.printStackTrace(); } return false; } }
当你用Java来声明bean的时候,可以使用这个自定义条件类:
@Conditional(JdbcTemplateCondition.class) @Service public MyService service() { ...... }
这个例子中只有当 JdbcTemplateCondition类的条件成立时才会创建MyService这个bean。也就是说MyService这bean的创建条件是 classpath里面包含 JdbcTemplate,否则这个bean的声明就会被忽略掉。
SpringBoot定义了很多有趣的条件,并把他们运用到了配置类上,这些配置类构成了 SpringBoot的自动配置的基础。SpringBoot运用条件化配置的方法是:定义多个特殊的条件化注解,并将它们用到配置类上。下面列出了 SpringBoot提供的部分条件化注解:
2.5、@ConfigurationProperties与@EnableConfigurationProperties
当某些属性的值需要配置的时候,我们一般会在 application.properties文件中新建配置项,然后在bean中使用 @Value注解来获取配置的值,比如下面配置数据源的代码。
// jdbc config jdbc.mysql.url=jdbc:mysql://localhost:3306/sampledb jdbc.mysql.username=root jdbc.mysql.password=123456 ...... // 配置数据源 @Configuration public class HikariDataSourceConfiguration { @Value("jdbc.mysql.url") public String url; @Value("jdbc.mysql.username") public String user; @Value("jdbc.mysql.password") public String password; @Bean public HikariDataSource dataSource() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(url); hikariConfig.setUsername(user); hikariConfig.setPassword(password); // 省略部分代码 return new HikariDataSource(hikariConfig); } }
使用 @Value注解注入的属性通常都比较简单,如果同一个配置在多个地方使用,也存在不方便维护的问题(考虑下,如果有几十个地方在使用某个配置,而现在你想改下名字,你改怎么做?)。对于更为复杂的配置,Spring Boot提供了更优雅的实现方式,那就是 @ConfigurationProperties注解。我们可以通过下面的方式来改写上面的代码:
@Component // 还可以通过@PropertySource("classpath:jdbc.properties")来指定配置文件 @ConfigurationProperties("jdbc.mysql") // 前缀=jdbc.mysql,会在配置文件中寻找jdbc.mysql.*的配置项 pulic class JdbcConfig { public String url; public String username; public String password; } @Configuration public class HikariDataSourceConfiguration { @AutoWired public JdbcConfig config; @Bean public HikariDataSource dataSource() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(config.url); hikariConfig.setUsername(config.username); hikariConfig.setPassword(config.password); // 省略部分代码 return new HikariDataSource(hikariConfig); } }
@ConfigurationProperties对于更为复杂的配置,处理起来也是得心应手,比如有如下配置文件:
#App app.menus[0].title=Home app.menus[0].name=Home app.menus[0].path=/ app.menus[1].title=Login app.menus[1].name=Login app.menus[1].path=/login app.compiler.timeout=5 app.compiler.output-folder=/temp/ app.error=/error/
可以定义如下配置类来接收这些属性
@Component @ConfigurationProperties("app") public class AppProperties { public String error; public List<Menu> menus = new ArrayList<>(); public Compiler compiler = new Compiler(); public static class Menu { public String name; public String path; public String title; } public static class Compiler { public String timeout; public String outputFolder; } }
@EnableConfigurationProperties注解表示对 @ConfigurationProperties的内嵌支持,默认会将对应Properties Class作为bean注入的IOC容器中,即在相应的Properties类上不用加 @Component注解。