Spring的非侵入性–基于Java的容器配置介绍
一、Spring框架的非侵入性
1.前言
在上一篇博文中讲到,Spring允许以非侵入方式使用注解,无需接触目标组件的源代码。 要想真正理解这句话,我们就有必要先弄清楚非侵入式设计的概念了。
2.侵入式与非侵入式
- 侵入式:代码结构要与所使用的技术产生依赖。
- 非侵入式:使用一个新的技术不会或者基本不改变原有代码结构,原有代码不作任何修改即可。
3.侵入式框架与非侵入式框架
-
侵入式框架:引入了框架,对现有的类的结构有影响,需要实现框架某些接口或者基础某些特定的类。
- 优点:侵入式可以使得用户的代码与框架更好的结合,充分利用框架提供的功能。
- 缺点:侵入式让用户的代码对框架产生了依赖,不利于代码的复用,当去除框架的时候,程序就无法运行。
- 例子:Struts1框架, Struts1代码严重依赖于Struts1 API,属于侵入性框架。
-
非侵入式框架:引入了框架,对现有的类结构没有影响,不需要实现框架某些接口或者特定的类。
- 优点:代码对框架没有过多的依赖,允许所开发出来的应用系统能够在不用的环境中自由移植,不需要修改应用系统中的核心功能实现的代码
- 缺点:无法复用框架提供的代码和功能
- 例子:Spring框架,通过配置完成依赖注入就可以使用,当我们想换个框架,只需要修改相应的配置,程序仍然可以运行。
二、在Java代码中使用注解来配置容器
1.基础概念:@Bean
和@Configuration
由@Configuration
修饰的类和由@Bean
修饰的方法组成了Spring Java配置的主要构件。
@Bean
注解用于方法实例化,配置和初始化要由Spring IoC容器管理的对象。 使用的时候,@Bean
注解的方法,所在的类需要用@Configuration
来修饰,表示该类是作为Bean定义的来源,容器初始化时,需要扫描该类,进行自动装配。
用@Configuration
注解的类,表示该类的主要目的是作为Bean定义的来源。 此外,@Configuration
类允许通过调用同一类中的其他@Bean
方法来定义Bean间的依赖关系。 最简单的@Configuration
类的内容如下:
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
2. @Component
和@Bean
的区别
@Component和@Bean都可以定义bean,但实际上@Component
和@Bean
是做两个完全不同的事情,不应该混为一谈,具体比较如下:
@Component
(和@Service
和@Repository
)用于自动检测和使用类路径扫描自动配置bean。 注释类和bean之间存在隐式的一对一映射(即每个类一个bean)。这种方法对需要进行逻辑处理的控制非常有限,因为它纯粹是声明性的。@Bean
用于显式声明单个bean,而不是让Spring像上面那样自动执行它。它将bean的声明与类定义分离,并允许您精确地创建和配置bean。
那我们在什么时候会使用@Bean
?假设下,某天你想创建一个第三方的组件,但是你没有源代码,也就没办法使用@Component
进行自动配置,这种时候使用@Bean
就比较合适了。 又比如,具体采用哪个实现是依赖于某些参数的,在这种情况下@Bean
无疑是更合适的选择。 示例如下:
@Bean
@Scope("prototype")
public SomeService someService(String state) {
switch (state) {
case 1:
return new Impl1();
case 2:
return new Impl2();
case 3:
return new Impl3();
default:
return new Impl();
}
}
3.使用AnnotationConfigApplicationContext实例化Spring容器
以下各节介绍了Spring 3.0中引入的AnnotationConfigApplicationContext
。 这种通用的ApplicationContext
实现,不仅能够接受@Configuration
类作为输入,而且还可以接受普通的@Component
类和带有JSR-330元数据注释的类。
- 当提供
@Configuration
类作为输入时,@Configuration
类本身将注册为Bean定义,并且该类中所有已声明的@Bean
方法也将注册为Bean定义。 - 提供
@Component
和JSR-330类时,它们将注册为bean定义,并且假定在必要时在这些类中使用了诸如@Autowired
或@Inject
之类的DI元数据。
(1)作为AnnotationConfigApplicationContext
构造函数的参数
将@Configuration类
用作输入,实例化AnnotationConfigApplicationContext
,使得Spring容器的使用,完全不依赖XML。 如下面的示例所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
前面还提到,同样可以将任何@Component
或JSR-330带注释的类作为输入,提供给AnnotationConfigApplicationContext的构造函数进行实例化,如以下示例所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
上面的示例假定MyServiceImpl,Dependency1和Dependency2使用了Spring依赖项注入注解,例如@Autowired
。
(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();
}
(3)使用scan(String ...)
启动组件扫描(常见)
要启用组件扫描,可以按如下方式使用@Configuration
注解类:
@Configuration
@ComponentScan(basePackages = "com.acme")
public class AppConfig {
...
}
在上面的示例中,对com.acme
包进行了扫描,以查找任何@Component
注解的类,并将这些类注册为容器中的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);
}
另外需要注意,@Configuration
类使用了@Component
进行元注解( meta-annotated with @Component
),因此它们也是组件扫描的候选对象。 在前面的示例中,假定AppConfig在com.acme包(或下面的任何包)中声明,则在调用scan()时也会将其扫描。 根据refresh(),AppConfig的所有@Bean方法都将在容器中进行处理并注册为Bean。
4.使用@Bean
注解
@Bean
是方法级别(method-level )的的注解,是对XML配置的<bean/>
节点的直接模拟。@Bean
注解同样支持<bean/>
提供的一些属性,如:init-method
、destroy-method
、autowiring
、name
。可以在@Configuration
注解的类或@Component
注解的类中使用@Bean
注释。
(1)声明一个bean
要声明bean,可以使用@Bean
注解,对类方法进行注释,这样子Spring就会在以方法返回值为指定类型的ApplicationContext
中定义一个bean。**默认情况下,bean名称与方法名称相同。**如以下示例:
@Configuration
public class AppConfig {
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
上面示例的配置等价于下面的XML配置
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>
以上两种方式的声明都使一个名为transferService
的bean在ApplicationContext
中可用,并绑定到类型为TransferServiceImpl
的对象实例,即transferService -> com.acme.TransferServiceImpl
还可以使用接口(或基类)作为返回类型来声明@Bean
方法,如以下示例所示:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
如果您通过声明的服务接口来一致地引用类型,则@Bean
返回类型可以安全地加入该设计决策。 但是,对于实现多个接口的组件或存在其实现类型潜在引用的组件,声明尽可能具体的返回类型(至少与引用您的bean的注入点所要求的具体类型一样)更为安全。
(2)bean依赖
@Bean
注解的方法可以具有任意数量的参数,这些参数描述构建该bean所需的依赖关系。 例如,如果我们的TransferService
需要一个AccountRepository
,则可以使用方法参数来实现该依赖关系,如以下示例所示:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
(3)接收生命周期回调
任何使用@Bean
注解定义的类都支持常规的生命周期回调,并且可以使用JSR-250中的@PostConstruct
和@PreDestroy
注解。
常规Spring生命周期回调也得到完全支持。 如果bean实现了InitializingBean
,DisposableBean
或Lifecycle
等接口,则容器将调用它们各自的方法。
还完全支持标准的* Aware
接口集(例如BeanFactoryAware
,BeanNameAware
,MessageSourceAware
,ApplicationContextAware
等)。
@Bean
注解还支持指定任意的初始化和销毁的回调方法,类似于XML配置中bean元素的init-method
和destroy-method
属性,如以下示例所示:
public class BeanOne {
public void init() {
// initialization logic
}
}
public class BeanTwo {
public void cleanup() {
// destruction logic
}
}
@Configuration
public class AppConfig {
@Bean(initMethod = "init")
public BeanOne beanOne() {
return new BeanOne();
}
@Bean(destroyMethod = "cleanup")
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
在bean的构造期间直接调用初始化方法也是可行的,不一定需要依赖容器的生命周期回调。 如以下示例所示:
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
BeanOne beanOne = new BeanOne();
beanOne.init();
return beanOne;
}
// ...
}
(4)指定Bean的作用域(Scope)
在用@Bean
定义bean时,可以指定bean的作用域,标准的作用域有:singleton、prototype、request、session、application、websocket,其中request、session、application、websocket,这四个作用域只有在web应用中才能够被使用到。
Scope | Description |
---|---|
singleton | (默认) 将每个Spring IoC容器的单个bean定义的作用域限定为单个对象实例(单例模式)。 |
prototype | 将单个bean定义的作用域限定为任意数量的对象实例。 |
request | 将单个bean定义的作用域限定为单个HTTP请求(request)的生命周期内。 也就是说,每个HTTP请求都会有一个单独的bean。仅在web应用中能使用到。 |
session | 将单个bean定义的作用域限定为HTTP会话(Session)的生命周期内。仅在web应用中能使用到。 |
application | 将单个bean定义的作用域限定为ServletContext 的生命周期内。仅在web应用中能使用到。 |
websocket | 将单个bean定义的作用域限定为WebSocket 的生命周期内。. 仅在web应用中能使用到。 |
-
使用
@Sopce
注解bean默认的作用域是singleton,但是可以使用
@Scope
注解进行自定义,以下示例,将bean的作用域指定为prototype:@Configuration public class MyConfiguration { @Bean @Scope("prototype") public Encryptor encryptor() { // ... } }
(5)自定义bean名称
默认情况下,配置类使用@Bean
方法的名称作为bean的名称。 但是可以使用name
属性进行自定义,以下示例将bean名称指定为myThing,默认为thing:
@Configuration
public class AppConfig {
@Bean(name = "myThing")
public Thing thing() {
return new Thing();
}
}
(6)bean别名
有时希望为单个Bean设置多个名称,为此,@Bean
注解的name
属性接受一个String数组。 以下示例显示了如何为bean设置多个别名:
@Configuration
public class AppConfig {
@Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
public DataSource dataSource() {
// instantiate, configure and return DataSource bean...
}
}
5.使用@Configuration
注解
@Configuration
是一个类级别(class-level )的注解,表示其对象是Bean定义的源。
(1)注入bean之间的依赖关系
当bean彼此依赖时,表示依赖关系就像让一个bean方法调用另一个一样简单,如以下示例所示:
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
在上面的示例中,beanOne
通过构造函数注入接收到beanTwo
的引用。
再次强调一遍:仅当在@Configuration类中声明@Bean方法时,此声明bean间依赖性的方法才有效。 您能使用普通的@Component类声明Bean间的依赖关系。
(2)查找 方法注入
不常用到,占个位置留着以后用到再补充。
(3)有关基于Java的配置内部中是如何工作的更多信息
不常用到,占个位置留着以后用到再补充。
6.组合基于java的配置
不常用到,占个位置留着以后用到再补充。
参考资料
[1]:Spring学习(1):侵入式与非侵入式,轻量级与重量级