Spring概述
Spring是一个开源的、轻量级的、容器化的用于构建企业级Java应用的框架。Spring的二进制分发包和源代码都是免费的,遵循Apache2 Licence。
开源
- Spring源代码:https://github.com/spring-projects/spring-framework
- Spring二进制分发包(使用Maven):http://mvnrepository.com/artifact/org.springframework
- Spring文档:http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle
在任何情况下,都推荐使用依赖管理系统(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的实现类在初始化的时候需要指定账户表数据,账户表在初始化的时候需要指定数据库信息。
1 | public class TransferServiceImpl implements TransferService { |
1 | public class JdbcAccountRepository implements AccountRepository { |
在这个例子中,我们可以通过建立Java Configuration类来完成这些配置工作。
1 |
|
Spring为我们提供了@Configuration和@Bean注解处理这类配置。
接下来,只需要告诉Spring通过这个Configuration来初始化应用的上下文就可以让整个应用run起来了。
1 | // Create application context from the configuration |
注意上面代码执行的时候,除了三个显示的Bean之外,ApplicationConfig也是一个Spring Bean:他用来注入其他Bean。
几种访问Bean的方式
从前面的代码中,我们看到使用IoC编程范式的三个步骤:
- 从configuration创建应用的上下文(依赖资源)
- 找到对应的Bean
- 使用Bean提供的方法实现业务逻辑
Spring框架本身提供了三种不同的方式来定位需要的Bean。
1 | ApplicationContext context = SpringApplication.run(...); |
其中第三种方法仅限Bean的名字没有歧义时候才可以用,如果在Configuration类中有两个以上的TransferService类型的Bean,使用第三种方法时Spring会报异常。
在Spring中使用多个Configuration
复杂的应用需要配置的Bean可能很多,这些上下文甚至由不同的人维护,都放在一个配置类中会导致混乱。比较好的实践是将他们分散在不同的配置类中,相关的Bean放在一起。一个常见的web应用的例子是将配置类分成三部分:应用本身的配置、web容器的配置和运行环境的配置。
1 |
|
1 |
|
使用@Import注解引用上面两个配置:
1 |
|
根据Pivotal给出的最佳实践,除了分离不同“类型”的Bean到不同的配置中外,将应用配置和环境配置分离会更好,这是因为应用通常会运行在不同的环境上:本地开发、集成测试、生产环境等等。
@Bean与@Autowired
Spring包含了两个描述依赖的注解:@Bean和@Autowired。
@Bean 和 @Autowired 做了两件完全不同的事情:
- @Bean 告诉 Spring:“这是这个类的一个实例,请保留它,并在我请求时将它还给我”。
- @Autowired 说:“请给我一个这个类的实例,例如,一个我之前用@Bean注释创建的实例”。
对于方法,两种形式的注解都会告知IoC逐个扫描方法的参数,并根据方法参数的类型自动完成注入。
下面是两种注入的例子:
1 |
|
在这个小例子里,使用者和提供者在一个class中,看起来没什么意义。在实际的大项目里,DemoService方法返回了IoC需要的Bean,通常会放在Configuration类中,业务代码里直接使用Service的地方则一般使用@Autowired直接注入。
不论使用哪种方法,都应当注意尽可能直接的表述各个模块的依赖关系,不要穿越层级去依赖。下面是一个坏例子:
java
1
2
3
4
5
6
7
8
9
public class ApplicationConfig {
public AccountService accountService( DataSource ds ) {
return new AccountService( accountRepository(ds) );
}
public AccountRepository accountRepository( DataSource ds ) {
return new JdbcAccountRepository( ds );
}
}实际上accountService并不直接依赖DataSource,比较好的写法可以清晰的描述AccountService –> AccountRepository –> DataSource相互依赖的关系,就像下面这样
java
1
2
3
4
5
6
7
8
9
public class ApplicationConfig {
public AccountService accountService( AccountRepository repo ) {
return new AccountService( repo );
}
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。
1 |
|
上面的代码会根据@Import的顺序,返回Config2中的Bean定义,输出Id=example2。
Bean的实现模式
单例singleton
Spring中所有的Bean默认都是单例,即不论有多少处地方有依赖,只要Bean类型相同,在程序中就都是唯一实例的引用。注解@Scope可以改变实现模式。下面的代码展示了默认的单例Bean行为:
1 | public AccountService accountService() { |
1 |
|
1 | AccountService service1 = (AccountService) |
Bean单例是Spring中Java Bean的默认模式,在大多数场景中,这是满足业务需求的,但在有些场景,单例会引入副作用,一个典型的场景是web后端应用:对于每一个Java请求,Web服务器会拉起一个新的Java线程处理请求,而这些线程会同时访问同一个Bean引用,这种多线程访问会导致意料外的异常行为。
为了解决这类问题,通常会采用三种策略:
- 将Bean设计为无状态或不可变的。
- 将可能被并发使用的方法通过
synchronized加锁,这通常会带来额外的复杂度- 使用其他Bean Scope
原型prototype
与单例不同,当Bean Scope设置为prototype时,每次bean被依赖引用,IoC容器都会生成Bean的一份全新实例。
1 |
|
1 | Action action1 = (Action) context.getBean("deviceAction"); |
特殊种类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的高级用法
外部属性值设置
我们的应用离不开各种各样的参数,一个典型的例子是连接数据库时使用的连接字符串、用户名和密码。显然,下面的配置方式存在很多问题:
1 |
|
把参数硬编码到代码中不仅仅会导致修改的不便,很多时候还会造成严重的安全问题,毕竟数据库密码通过github这样的托管中心泄露的事情已经发生过不止一次了。更好的做法是将这些信息保存在外部的配置文件中。
为了实现这一点,Spring提供了一个特殊的Bean:Environment,这个Bean会在Spring程序启动时从程序的运行时环境中加载外部属性。外部属性可以通过不同的渠道获取,优先级顺序如下:
- JVM System Properties:相当于
System.getProperty()得到的值。 - Servlet Context Parameters
- JNDI
- 系统环境变量:相当于
System.getenv()得到的值。 - Java配置文件
我们可以在项目中创建app.properties文件,并补充数据库连接的内容:
1 | db.driver=org.postgresql.Driver |
这样在Configuration中,就可以通过Enviroument Bean获取这些信息:
1 | public class DbConfig { |
除了从项目根目录获取约定的的app.properites配置文件外,我们还可以通过@PropertySource注解来指定Environment Bean需要加载的配置文件位置:
1 |
|
为了进一步简化代码,Spring提供了@Value注解,避免了冗余的Environment Bean操作:
1 |
|
配置组Profile
配置是应用中易变的部分,可能需要根据环境(dev、test、production)、实现(jpa、jdbc)、部署环境(on-premise、cloud)等的不同,对Configuration做出相应的调整。Spring使用@Profile配置组注解来抽象这些易变的配置。
@Profile注解可以注释在Configuration类上,也可以单独注释在某一个@Bean方法上。
1 |
|
1 |
|
在下面的例子中,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注解指定配置组的那部分配置始终生效,不论当前激活是什么配置组。


