如何有效地配置基于Spring的应用系统

时间:2024-01-03 11:19:02

Spring为应用系统的开发提供了极大的方便,其IoC反向注入(或DI依赖注入)的概念也彻底地改变了旧的编程方式,让我们只需关注如何使用对象,而创建对象交给Spring去完成,即把使用对象和创建对象分离。

同时,Spring也为我们提供了创建对象的多种配置方法。以前我们大都用XML来配置,而在Spring 3.1后用Java代码配置已能完全取代XML配置(用Java代码配置也称为JavaConfig)。新的应用系统建议都采用JavaConfig方式进行配置。用JavaConfig比用XML来配置的好处是让程序员更容易地进行配置,以及更容易地找到在哪里配置的。

不过,对于大型系统其配置分散在多处,大量的@Autowired让程序员有时也难以琢磨依赖是从哪里注入进来的。比如,下面的@Autowired:

@Configuration
public class AppConfig {
@Autowired
DataSource dataSource; ...
}

@Autowired将指示Spring从其容器里注入依赖对象,但程序员有时会关心该依赖项是在哪里定义的,以便检查或修改。

这篇文章说到了一种可行的方法。即先定义一个接口:

public interface DataConfig {
DataSource dataSource();
}

然后实现该接口:

@Configuration
public class JndiDataConfig implements DataConfig {
@Bean
public DataSource dataSource() {
...
}
}

最后再注入该接口:

@Configuration
public class AppConfig {
@Autowired
private DataConfig dataConfig; @Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataConfig.dataSource());
}
}

注意,这里不是直接注入DataSource依赖项,而是间接注入DataConfig接口,并通过该接口去获取依赖项。

该文章也说到,这样做的好处是,在Eclipse里我们可以通过Ctrl+T来查看DataConfig的实现类有哪些,这样就可以方便地知道DataConfig是在哪里定义的了。

同时该文章还说到可以用注解来进一步显化配置,请查看该文章的@Dev部分,这里不再赘述。

本人今天也在思考相同的问题,即如何让程序员更容易地找到配置的地方,也正好想到基于接口的配置方法。书写本文时发现了这篇文章,深感认同。不过,这里还有个小问题可能产生隐患。上面我们实现了DataConfig接口,再摘抄如下:

@Configuration
public class JndiDataConfig implements DataConfig {
@Bean
public DataSource dataSource() {
...
}
}

在Eclipse中我们经常是先书写public class ... 这一行,然后用Ctrl+1来自动生成实现接口的方法,再然后才开始写自己的代码,而此时就很容易忘记加上@Bean注解了,尤其是在DataConfig接口有大量方法的情况下更容易疏忽。一旦没加上@Bean注解则在AppConfig中获取到的DataSource(dataConfig.dataSource())依赖项就是一个new出来的普通对象,而不是一个Spring Bean,这将导致两个问题:

  1. 每次调用dataConfig.dataSource()都将重新创建DataSource对象。这或许不是我们希望的结果。
  2. 虽然这个例子注入的DataSource对象不太可能需要Spring AOP拦截,但如果我们需要注入的对象是一个需要被AOP拦截的对象(如需要事务控制)则将造成假象,因为它只是一个new出来的普通对象,而不是Spring Bean,此时Spring并不会对该对象进行AOP处理。

当然,即使@Configuration类不实现某个接口,只要其方法上没加上@Bean注解都会有此问题,但让@Configuration类实现某个接口后,再结合Eclipse IDE工具的方便性,我们就更加容易地忘记加上@Bean注解。忘记加上@Bean注解导致的系统隐患是,系统仍能运行但失去了Spring为我们提供的诸如AOP事务控制的功能,将产生不可预计的后果,并且很难去发现原因。

为应对此问题,经实测我们可以在接口上增加@Bean注解,如下:

public interface DataConfig {
@Bean
DataSource dataSource();
}

此后一切不变,我们仍然要在实现该接口的@Configuration类(即上面的JdbcDataConfig类)的方法加上@Bean(Eclipse不会自动加上)。而一旦忘记加则上面AppConfig中的dataConfig.dataSource()调用将报错:No bean named 'dataSource' is defined。

让系统在启动时报错也是一种消除隐患的方法。:)

上面,我们用接口来定义要配置的内容,这种做法还有另一个使用场景。试想一下某个系统有基础层和应用层两层,基础层提供了很多模块,有些模块应用层要用,有些不用。并且假设基础层的所有模块都在同一个jar包中。我们该如何配置基础层呢?此时需要考虑两个问题:

  1. 如果用@ComponentScan来扫描基础层,则那些不需要的模块也将被扫描出来。如果这些不需要的模块必须配置才能启动起来则造成整个系统无法启动。因此,我们要避免用@ComponentScan来扫描基础层(以及第三方软件),而只去扫描应用层。
  2. 基础层和应用层大都是由不同团队负责开发,如何让应用层团队做好基础层模块的配置呢?一种做法是口头交流,更好的做法是代码级交流。

利用上面的基于接口的配置方法可有效地解决配置的难题。

首先,基础层为每个模块都定义独自的配置接口,应用层团队只要实现该配置接口即可,这样就解决了交流的问题。其次,基础层的每个模块都有一个入口配置,应用层只需@Import进去即可。

如果参照Spring MVC的做法,我们也可以为每个模块创建一个@EnableModule1、@EnableModule2等这样的注解,让配置更加显化。

通过接口来显化要配置的内容对于多人多团队开发将起到积极的作用,使整个系统的配置过程更加清晰,配置与实际运行更加可控,遇到问题时更容易找到配置点。