avatar

目录
Spring中的Java Configuration

Spring概述

Spring是一个开源的、轻量级的、容器化的用于构建企业级Java应用的框架。Spring的二进制分发包和源代码都是免费的,遵循Apache2 Licence。

开源

在任何情况下,都推荐使用依赖管理系统(Maven、Gradle或者Ant/Ivy)使用Spring。

轻量

Spring不需要一个Java EE应用服务器来运行,当然Spring框架构建的程序可以运行在Java EE服务器上。

Spring是一个非侵入式的框架,使用Spring构建项目通常不需要去继承框架内部的类或者实现框架接口,仅仅需要编写POJOs即可。

当然Spring从体积上看也很小,常用的jar包大小不到8MiB。

容器

Spring为Java代码中的各种对象提供了一个依赖注入(Dependency Injection, DI)的运行环境,这个环境有一点类似docker container:提供了程序的依赖管理和生命周期管理。

对于企业级应用来说,Java代码需要使用的技术可能非常广泛并且底层,包括jdbc、事务、SQL和NoSQL访问、消息队列……这个列表可能随着时间的推移不断变长,Spring会从框架的角度,通过接口、框架类和注解来简化业务代码使用这些底层技术时的工作量。

Spring容器的工作原理

(http://ww1.sinaimg.cn/large/bf777c88gy1gcwvudvcldj20xg0myjvw.jpg)

控制反转

通常写代码的思路是,先准备资源,比如数据库连接,文件系统等等,再实现业务逻辑,最后把结果返回出去。如果用面向对象的思路来做的话,这些业务逻辑依赖的资源基本上需要作为类的成员,在构造函数或者专用的初始化方法中完成初始化。在Spring的语境里,这些资源被称作上下文

在实际的业务中,承载业务逻辑代码的POJOs可能有几十上百个,由不同的人开发,交给不同的人维护。业务逻辑和资源之间的依赖关系会变得复杂无比,越来越多的代码冗余在这些上下文资源的初始化上,Java世界面对这个问题的解决方案就是控制反转(IoC—Inversion of Control)。

简单来说,如果有个管理中心负责所有用到的上下文的初始化,业务逻辑只管拿来用,不再负责“控制”上下文,这种思想就是控制反转。举个例子:如果在家做菜,我们可能需要从菜市场买菜、洗菜择菜,然后才能准备下锅操作。如果只有一两个人做菜,这样做是完全没问题的,可这种思路不适用于饭店:如果没有专门的采购、洗菜工、打荷、切墩,所有工序都需要厨师亲自出马,那饭店后厨绝对会变成OVER COOKED里面的场景:(。对于厨师来说,一道菜的“上下文”交给专业的员工完成显然是更高效的组织形式。更关键的是,假如有客人不吃猪肉要换成牛肉,我们只需要把上下文的“肉资源”从猪肉换成牛肉即可,至于牛肉是从菜市场卖还是直接日本空运,那是别人考虑的事情。

依赖注入

实现控制反转需要一个“管理中心”接管资源的初始化,Spring就是就是这样一个IoC管理中心。从前面的描述中,我们发现初始化这些上下文的一个重点就是在程序运行时,动态的提供上下文资源。Spring是通过依赖注入(Dependency Injection)技术来实现控制反转的。这里有四个核心问题:

  • 依赖于谁:当然是应用程序依赖于IoC容器
  • 为什么需要依赖:应用程序需要外部资源(上下文)
  • 谁注入谁:IoC容器把对象注入给应用程序
  • 注入了什么:应用程序所需的外部资源

虽然看上去比较废话,但实际的项目中,这些依赖并非像大多数语言(比如C++)在编译器就确定好,而是在程序运行时由Spring IoC容器动态完成。

为什么管理中心称作容器?IoC容器和Docker容器的含义是类似的:提供一个应用运行可用的上下文。

Configuration

随着资源初始化的权限由具体的业务代码交给IoC容器,一些参数性质的信息也需要通过某些方式交给IoC容器,Configuration就是用来做这件事的。远古时代的Spring Configuration是真正的配置文件:通常是当时流行的XML文件。如今开发Spring应用大多数时候通过Spring提供的注解完成配置。

银行转账的例子

说完了依赖注入和Configuration的意义,下面举一个来自Pivotal课程中的转账例子来说明依赖注入是怎么通过Java Configuration来进行的。

业务场景是常见的银行转账,我们的程序大致分成两层:接口层提供转账服务,数据抽象层完成数据库到Java对象的适配。

依赖关系

使用Spring注解

用Java代码来表述这种依赖关系,就是TransferService的实现类在初始化的时候需要指定账户表数据,账户表在初始化的时候需要指定数据库信息。

java
1
2
3
4
5
6
public class TransferServiceImpl implements TransferService {
public TransferServiceImpl(AccountRepository ar) {
this.accountRepository = ar;
}

}
java
1
2
3
4
5
6
public class JdbcAccountRepository implements AccountRepository {
public JdbcAccountRepository(DataSource ds) {
this.dataSource = ds;
}

}

在这个例子中,我们可以通过建立Java Configuration类来完成这些配置工作。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class ApplicationConfig {
@Bean public TransferService transferService() {
return new TransferServiceImpl( accountRepository() );
}
@Bean public AccountRepository accountRepository() {
return new JdbcAccountRepository( dataSource() );
}
@Bean public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.postgresql.Driver");
dataSource.setUrl("jdbc:postgresql://localhost/transfer" );
dataSource.setUsername("transfer-app");
dataSource.setPassword("secret45");
return dataSource;
}
}

Spring为我们提供了@Configuration@Bean注解处理这类配置。

接下来,只需要告诉Spring通过这个Configuration来初始化应用的上下文就可以让整个应用run起来了。

java
1
2
3
4
5
6
7
8
// Create application context from the configuration
ApplicationContext context =
SpringApplication.run( ApplicationConfig.class );
// Look up a service
TransferService service =
context.getBean("transferService", TransferService.class);
// Use the service
service.transfer(...);

注意上面代码执行的时候,除了三个显示的Bean之外,ApplicationConfig也是一个Spring Bean:他用来注入其他Bean。

几种访问Bean的方式

从前面的代码中,我们看到使用IoC编程范式的三个步骤:

  1. 从configuration创建应用的上下文(依赖资源)
  2. 找到对应的Bean
  3. 使用Bean提供的方法实现业务逻辑

Spring框架本身提供了三种不同的方式来定位需要的Bean。

java
1
2
3
4
5
6
7
ApplicationContext context = SpringApplication.run(...);
// Use bean id, a cast is needed
TransferService ts1 = (TransferService) context.getBean("transferServic"”);
// Use typed method to avoid casting
TransferService ts2 = context.getBean("transferService", TransferService.class);
// No need for bean id if type is unique
TransferService ts3 = context.getBean(TransferService.class );

其中第三种方法仅限Bean的名字没有歧义时候才可以用,如果在Configuration类中有两个以上的TransferService类型的Bean,使用第三种方法时Spring会报异常。

在Spring中使用多个Configuration

复杂的应用需要配置的Bean可能很多,这些上下文甚至由不同的人维护,都放在一个配置类中会导致混乱。比较好的实践是将他们分散在不同的配置类中,相关的Bean放在一起。一个常见的web应用的例子是将配置类分成三部分:应用本身的配置、web容器的配置和运行环境的配置。

java
1
2
3
4
@Configuration
public class ApplicationConfig {
...
}
java
1
2
3
4
@Configuration
public class WebConfig {
...
}

使用@Import注解引用上面两个配置:

java
1
2
3
4
5
@Configuration
@Import({ApplicationConfig.class, WebConfig.class })
public class InfrastructureConfig {
...
}

根据Pivotal给出的最佳实践,除了分离不同“类型”的Bean到不同的配置中外,将应用配置和环境配置分离会更好,这是因为应用通常会运行在不同的环境上:本地开发、集成测试、生产环境等等。

@Bean与@Autowired

Spring包含了两个描述依赖的注解:@Bean和@Autowired。

@Bean 和 @Autowired 做了两件完全不同的事情:

  • @Bean 告诉 Spring:“这是这个类的一个实例,请保留它,并在我请求时将它还给我”。
  • @Autowired 说:“请给我一个这个类的实例,例如,一个我之前用@Bean注释创建的实例”。

对于方法,两种形式的注解都会告知IoC逐个扫描方法的参数,并根据方法参数的类型自动完成注入。

下面是两种注入的例子:

java
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootApplication
public class Application {

@Autowired
DemoService demoService;

@Bean
DemoService DemoService() {
return new DemoServiceImpl();
}
....
}

在这个小例子里,使用者和提供者在一个class中,看起来没什么意义。在实际的大项目里,DemoService方法返回了IoC需要的Bean,通常会放在Configuration类中,业务代码里直接使用Service的地方则一般使用@Autowired直接注入。

不论使用哪种方法,都应当注意尽可能直接的表述各个模块的依赖关系,不要穿越层级去依赖。下面是一个坏例子:

java
1
2
3
4
5
6
7
8
9
@Configuration
public class ApplicationConfig {
@Bean public AccountService accountService( DataSource ds ) {
return new AccountService( accountRepository(ds) );
}
@Bean public AccountRepository accountRepository( DataSource ds ) {
return new JdbcAccountRepository( ds );
}
}

实际上accountService并不直接依赖DataSource,比较好的写法可以清晰的描述AccountService –> AccountRepository –> DataSource相互依赖的关系,就像下面这样

java
1
2
3
4
5
6
7
8
9
@Configuration
public class ApplicationConfig {
@Bean public AccountService accountService( AccountRepository repo ) {
return new AccountService( repo );
}
@Bean public AccountRepository accountRepository( DataSource ds ) {
return new JdbcAccountRepository( ds );
}
}

重载

Spring本身支持定义多个同id的Bean,IoC容器会按照定义的顺序依次扫描Configuration,并使用重复id最后一次定义时的Bean。

注意!重载Bean是Spring的特性,为了避免意外重载,这个特性在SpringBoot中并未默认开启,如果需要开启重载特性,需要配置spring.main.allow-bean-definition-overriding=true

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class Config1 {
@Bean
public String example() {
return new String("example1");
}
}

public class Config2 {
@Bean
public String example() {
return new String("example2");
}
}

@Import({ Config1.class, Config2.class })
public class TestApp {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(TestApp.class);
System.out.println("Id=" + context.getBean("example"));
}
}

上面的代码会根据@Import的顺序,返回Config2中的Bean定义,输出Id=example2

Bean的实现模式

单例singleton

Spring中所有的Bean默认都是单例,即不论有多少处地方有依赖,只要Bean类型相同,在程序中就都是唯一实例的引用。注解@Scope可以改变实现模式。下面的代码展示了默认的单例Bean行为:

java
1
2
3
@Bean public AccountService accountService() {
return ...
}
java
1
2
3
4
5
@Bean  
@Scope("singleton") //also a singleton scope
public AccountService accountService() {
return ...
}
java
1
2
3
4
AccountService service1 = (AccountService)
context.getBean("accountService");
AccountService service2 = (AccountService) context.getBean("accountService");
assert service1 == service2; // True – same object

Bean单例是Spring中Java Bean的默认模式,在大多数场景中,这是满足业务需求的,但在有些场景,单例会引入副作用,一个典型的场景是web后端应用:对于每一个Java请求,Web服务器会拉起一个新的Java线程处理请求,而这些线程会同时访问同一个Bean引用,这种多线程访问会导致意料外的异常行为。
为了解决这类问题,通常会采用三种策略:

  1. 将Bean设计为无状态或不可变的。
  2. 将可能被并发使用的方法通过synchronized加锁,这通常会带来额外的复杂度
  3. 使用其他Bean Scope

原型prototype

与单例不同,当Bean Scope设置为prototype时,每次bean被依赖引用,IoC容器都会生成Bean的一份全新实例。

java
1
2
3
4
5
@Bean
@Scope("prototype")
public Action deviceAction() {
return
}
java
1
2
3
Action action1 = (Action)  context.getBean("deviceAction");
Action action2 = (Action) context.getBean("deviceAction");
assert action1 != action2; // True – different objects

特殊种类session和request

除了单例和原型外,在web应用中还经常需要另外两种Bean scope:session和request。

  • session:每个用户会话共用一份Bean实例,不同的用户会话会创建不同的实例。
  • request:每个请求公用一份Bean实例,不同请求会创建不同的实例。

显然,这是两种只在web应用中存在的Bean实现模式。

其他Bean实现模式

下面是Pivotal给出的Bean scope列表参考。

Scope Description
singleton Lasts as long as its ApplicationContext
prototype getBean() returns a new bean every time. Lasts as long as you refer to it, then garbage collected
session Lasts as long as user’s HTTP session
request Lasts as long as user’s HTTP request
application Lasts as long as the ServletContext (Spring 4.0)
global Lasts as long as a global HttpSession in a Portlet application (obsolete from Spring 5)
thread Lasts as long as its thread – defined in Spring but not registered by default
websocket Lasts as long as its websocket (Spring 4.2)
refresh Can outlive reload of its application context. Difficult to do well, assumes Spring Cloud Configuration Server

Spring Configuration的高级用法

外部属性值设置

我们的应用离不开各种各样的参数,一个典型的例子是连接数据库时使用的连接字符串、用户名和密码。显然,下面的配置方式存在很多问题:

java
1
2
3
4
5
6
7
8
9
@Bean
public DataSource dataSource() {
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName("org.postgresql.Driver");
ds.setUrl("jdbc:postgresql://localhost/transfer");
ds.setUser("transfer-app");
ds.setPassword("secret45");
return ds;
}

把参数硬编码到代码中不仅仅会导致修改的不便,很多时候还会造成严重的安全问题,毕竟数据库密码通过github这样的托管中心泄露的事情已经发生过不止一次了。更好的做法是将这些信息保存在外部的配置文件中。

为了实现这一点,Spring提供了一个特殊的Bean:Environment,这个Bean会在Spring程序启动时从程序的运行时环境中加载外部属性。外部属性可以通过不同的渠道获取,优先级顺序如下:

  1. JVM System Properties:相当于System.getProperty()得到的值。
  2. Servlet Context Parameters
  3. JNDI
  4. 系统环境变量:相当于System.getenv()得到的值。
  5. Java配置文件

我们可以在项目中创建app.properties文件,并补充数据库连接的内容:

ini
1
2
3
4
db.driver=org.postgresql.Driver 
db.url=jdbc:postgresql:localhost/transfer
db.user=transfer-app
db.password=secret45

这样在Configuration中,就可以通过Enviroument Bean获取这些信息:

java
1
2
3
4
5
6
7
8
9
10
@Configuration public class DbConfig {
@Bean public DataSource dataSource(Environment env) {
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName( env.getProperty( "db.driver" ));
ds.setUrl( env.getProperty( "db.url" ));
ds.setUser( env.getProperty( "db.user" ));
ds.setPassword( env.getProperty( "db.password" ));
return ds;
}
}

除了从项目根目录获取约定的的app.properites配置文件外,我们还可以通过@PropertySource注解来指定Environment Bean需要加载的配置文件位置:

java
1
2
3
4
5
6
@Configuration
@PropertySource ( "classpath:/com/organization/config/app.properties" )
@PropertySource ( "file:config/local.properties" )
public class ApplicationConfig {
...
}

为了进一步简化代码,Spring提供了@Value注解,避免了冗余的Environment Bean操作:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class DbConfig {
@Bean
public DataSource dataSource(
@Value("${db.driver}") String driver,
@Value("${db.url}") String url,
@Value("${db.user}") String user,
@Value("${db.password}") String pwd) {
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName( driver);
ds.setUrl( url);
ds.setUser( user);
ds.setPassword( pwd ));
return ds;
}
}

配置组Profile

配置是应用中易变的部分,可能需要根据环境(dev、test、production)、实现(jpa、jdbc)、部署环境(on-premise、cloud)等的不同,对Configuration做出相应的调整。Spring使用@Profile配置组注解来抽象这些易变的配置。

@Profile注解可以注释在Configuration类上,也可以单独注释在某一个@Bean方法上。

java
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@Profile("embedded")
public class DevConfig {
@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setName("testdb")
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:/testdb/schema.db")
.addScript("classpath:/testdb/test-data.db").build();
}
...
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class DataSourceConfig {
@Bean(name="dataSource")
@Profile("embedded")
public DataSource dataSourceForDev() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setName("testdb") ...
}
@Bean(name="dataSource")
@Profile("!embedded")
public DataSource dataSourceForProd() {
BasicDataSource dataSource = new BasicDataSource();
...
return dataSource;
}
...

在下面的例子中,DataSource这个Bean在embedded和非embedded两个配置组里都由定义,但embedded配置组不可能同时被同时激活和禁用,因此在实际运行时,只会有一份配置生效。这里@Profile("!embedded")是一种表达式写法,表示非embedded配置组时,下面的配置被激活。

配置组只有在运行时才能被启用,有几种不同的方法可以启用一个配置组:

  • 在命令行中添加系统参数:-Dspring.profiles.active=embedded,jpa参数会激活embedded和jpa这两个Profile
  • 在代码中指定系统参数:System.setProperty("spring.profiles.active", "embedded,jpa");,这也会激活embedded和jpa这两个Profile
  • 在集成测试场景中,还可以使用@ActiveProfiles注解来激活Profile

注意:未使用@Profile注解指定配置组的那部分配置始终生效,不论当前激活是什么配置组。

打赏
  • 微信
    微信
  • 支付宝
    支付宝