Bean装配
Spring框架中,我们通过声明方式装配SpringBean,声明的内容包括SpringBean的类全名、字段属性、和其他SpringBean的依赖关系、生命周期回调函数等。具体可以使用以下几种方式编写装配声明:
xml配置文件方式- JavaConfig方式
- 配置包扫描并使用注解方式
在Spring的早期版本,xml是唯一支持的方式。随着Java版本的更新,引入了注解功能后,Spring也随之支持了注解和JavaConfig方式,这几种方式我们都需要熟练掌握。以上几种方式也可以结合使用,实现更加灵活的配置方式。
基于XML进行Bean装配
引入XML Schema
在编写SpringBean声明之前,Spring的xml配置文件需要引用Schema,使用不同的功能可能需要加载不同的Schema文件。下面例子中,我们引入了一些基础Schema:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
如果没有正确的引入Schema,我们的IDE智能提示不会起作用,Spring框架也无法正确启动。
当然,我们完全没必要记住这些繁琐的配置,具体使用到哪个模块时,查询对应文档即可。
SpringBean定义
我们之前曾编写过类似如下代码进行Bean装配:
<bean id="toolConfig" class="com.gacfox.demo.ToolConfig">
<property name="mode" value="promiscuous" />
</bean>
<bean>标签:用于声明一个SpringBean定义id属性:SpringBean的唯一标识符,容器内唯一,如果出现重复则Spring报错,无法启动class属性:指定SpringBean的类全名name属性:name属性容易和id混淆,实际上一个SpringBean可以指定多个name,未指定id情况下其第一个name将作为id,未指定name情况下,Spring取类名的CamelCase写法作为默认name,多个相同name的SpringBean声明会相互覆盖,不建议这样定义
这里注意id和name的用法,不要搞混了。
属性注入
我们声明SpringBean后,还需要声明对象字段属性值、依赖的Bean等信息,Spring框架支持属性注入和构造函数注入两种方式对这些信息进行声明。不过构造函数注入因为不能解决循环依赖问题,我们最佳实践一般都是全部使用属性注入。
字面值注入
<bean id="toolConfig" class="com.gacfox.demo.ToolConfig">
<property name="mode" value="promiscuous" />
</bean>
<property>标签:声明一个属性注入信息name属性:声明注入的字段名value属性:声明注入的字段值
上面我们ToolConfig的mode属性是一个字符串(String)类型的字段,Java中的基本数据类型及封装类,都可以直接使用字面值方式进行装配。
除此之外,
SpringBean类型引用注入
<bean id="myTool" class="com.gacfox.demo.MyTool">
<property name="toolConfig" ref="toolConfig" />
</bean>
ref属性:用于属性注入的字段也是一个SpringBean的情况,通过id方式声明关联注入的依赖Bean
集合类型注入
List、Set、Map也可以直接通过配置的方式进行注入。
<bean id="fruits" class="com.gacfox.demo.Fruits">
<property name="fruitList">
<list>
<value>苹果</value>
<value>香蕉</value>
<value>梨</value>
</list>
</property>
</bean>
- 标签:定义一个列表
Set和List比较像,只不过是把<list>标签换成了<set>标签。Map则稍有不同:
<bean id="fruitsTranslate" class="com.gacfox.demo.FruitsTranslate">
<property name="fruitsMap">
<map>
<entry key="苹果" value="apple" />
<entry key="香蕉" value="banana" />
<entry key="梨" value="pear" />
</map>
</property>
</bean>
以上都是一些Spring规定的XML格式问题,我们完全没必要背下来,姑且一看学习其思想,使用时具体查询文档即可。
SpringBean作用域
默认情况下,Spring注入的Bean都是单例的,即多次注入的都是同一个实例,但是这有时不符合我们的要求。例如一个多线程模块,单例可能引起线程安全问题,这就需要我们显式配置Bean的作用域了。
Spring可配置的作用域有如下几种:
singleton:单例,每次依赖注入使用的都是同一个实例,在应用上下文启动时自动创建,默认值prototype:原型,每次依赖注入都会创建新的实例session:会话,Web环境下对应一次HTTP会话(仅用于WebApplicationContext)request:请求,Web环境下对应一次HTTP请求(仅用于WebApplicationContext)
如果我们没有用到Web相关功能,则只有singleton和prototype可用,前两种也是最常用的,我们必须熟练掌握。
<bean id="toolConfig" class="com.gacfox.demo.ToolConfig" scope="prototype">
<property name="mode" value="promiscuous" />
</bean>
上面代码中,我们声明toolConfig这个SpringBean为原型模式,如果我们用applicationContext.getBean()方法进行获取,我们会发现每次获取的对象都是新创建的。
通过动态代理注入依赖SpringBean
虽然我们现在已经基本掌握了Spring的装配机制,但是先别高兴的太早,考虑这样一种场景:单例模式的BeanA,依赖于原型模式的BeanB,我们的BeanA会在应用上下文启动时创建,而此时还没有BeanB对象,那么注入显然不会成功。但从业务逻辑上来说,这肯定是合理的诉求!
此时需要使用动态代理,将BeanB的代理注入给BeanA,至到其取用BeanB时再真正创建BeanB。
<bean id="beanB" class="com.gacfox.demo.BeanB" scope="prototype">
<aop:scoped-proxy />
</bean>
<bean id="beanA" class="com.gacfox.demo.BeanA">
<property name="beanB" ref="beanB" />
</bean>
代码中,<aop:scoped-proxy />声明为beanB对象生成了一个动态代理,这样就解决了beanA初始化时的beanB注入问题。
注:Spring支持基于接口的动态代理和基于CGLIB的动态代理两种方式,后者使用简单、性能更好,一般都默认使用该种方式。
通过外部属性文件载入SpringBean字面值
实际开发中,DAO层的数据源(DataSource)、Redis客户端、消息队列客户端等,一般都是通过SpringBean的方式整合到Spring框架中的。然而,这些组件包含URL、端口、密码等信息,这些信息直接写在XML中十分不方便管理,此时我们可以通过外部属性文件方式载入这些信息。
这需要用到property-placeholder功能。下面是一个例子:
data.properties
host=127.0.0.1
port=8080
password=root
<context:property-placeholder location="classpath:data.properties" />
<bean name="myClient" class="com.gacfox.demo.MyClient">
<property name="host" value="${host}" />
<property name="port" value="${port}" />
<property name="password" value="${password}" />
</bean>
上面代码中,我们配置了context:property-placeholder功能,其中location属性指定了配置文件的位置。SpringBean装配定义中,我们在property里通过占位符的方式,引用了属性文件中的key,其值就会自动以字面值方式进行装配了。
XML配置文件的互相引用
在实际开发中,由于工程结构比较庞大,整个工程代码一般都是划分模块的,Spring配置文件通常也建议按模块进行划分。一般来说,我们会有一个总配置文件,其他配置文件都通过总配置文件引入。引用配置文件写法例子如下:
<import resource="classpath:spring-submodule.xml"/>
上面代码中,我们引入了类路径下名为spring-submodule.xml的配置文件。
基于注解方式进行Bean装配
JDK1.5引入了注解这个新特性,Spring框架中也充分利用了这个功能,使用注解能够极大简化我们的Bean配置。
在声明Bean前,我们首先需要了解注解声明Bean的原理:其实很简单,我们通过配置告诉Spring框架一个包名,Spring框架启动时通过JDK的反射机制,扫描包下的类,如果是指定了注解的类,那么就创建BeanDefinition进行装配,这样就实现注解扫描装配了。
首先,我们需要在xml配置文件中启用注解扫描功能:
<context:component-scan base-package="com.gacfox.demo" />
然后在指定的包中,使用@Component注解标注即可:
package com.gacfox.demo;
import org.springframework.stereotype.Component;
@Component("myComponent")
public class MyComponent {
}
这样我们就声明了一个SpringBean。该写法和如下XML方式完全等同:
<bean id="myComponent" class="com.gacfox.demo.MyComponent" />
除了@Component,Spring框架也支持@Controller,@Service,@Repository这三个注解,它们没有什么特殊功能,只是名字不同。这三个注解对应于JavaEE工程的三层架构(Controller - Service - DAO),实际开发中我们建议对应组件使用这三个特别名字的注解进行标注,这样我们的代码会更加清晰。
注意:@Component注解的value并非必须参数,其默认值为类名的CamelCase写法,最佳实践建议省略该名,保持默认值。
注解方式标注作用域
之前XML方式介绍了Bean的作用域,注解方式可以使用@Scope来标注:
@Scope("prototype")
@Component("myComponent")
public class MyComponent {
}
注解方式和XML方式类似,作用域默认为单例。
注解方式引用资源文件
类似XML方式,注解方式装配时可以使用@Value注解引用外部属性文件。下面是一个例子:
package com.gacfox.demo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class MyClient {
@Value("${host}")
private String host;
@Value("${port}")
private String port;
@Value("${password}")
private String password;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getPort() {
return port;
}
public void setPort(String port) {
this.port = port;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "MyClient{" +
"host='" + host + '\'' +
", port='" + port + '\'' +
", password='" + password + '\'' +
'}';
}
}
注意此种方式也必须在XML配置文件中开启context:property-placeholder功能。
基于注解自动装配
Spring框架支持自动装配,也就是不需要声明,自动根据字段类型注入SpringBean。下面是一个例子:
package com.gacfox.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DemoService {
@Autowired
private MyComponent myComponent;
public void test() {
System.out.println(myComponent);
}
}
@Autowired注解声明Bean中该字段需要自动装配,默认使用by-type进行依赖注入,也就是基于类型来自动注入。
自动装配时可能遇到一个问题,@Autowired因为默认是by-type注入的,如果同一个Java类型有多个Bean存在,这时就会发生问题。此时,我们可以用@Qualifier注解再指定Bean名:
package com.gacfox.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component
public class DemoService {
@Autowired
@Qualifier("myComponent")
private MyComponent myComponent;
public void test() {
System.out.println(myComponent);
}
}
注:
- 上面介绍XML方式装配时,我们没有介绍自动注入特性,因为XML装配的优势就是配置内容集中、直观,自动装配使用非常方便,但比较隐晦,不适合XML中配置的SpringBean。
@Autowired是Spring框架提供的注解,Spring也支持JSR-250标准的@Resource注解和JSR-330标准的@Inject注解,不过功能类似,非特殊需求没有必要使用。
基于JavaConfig进行Bean装配
除了XML和注解方式,还有一种基于JavaConfig的方式,这种方式和XML比较类似,只不过把XML声明配置换成了Java代码。下面是一个例子:
package com.gacfox.demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfig {
@Bean(name = "myComponent")
public MyComponent myComponent() {
return new MyComponent();
}
}
上面代码中,我们用@Configuration标注了一个配置类,其中一个方法用@Bean标注,这等同于XML方式中的<bean>标签。
注意:该种方式也需要在XML配置文件中开启注解扫描,否则扫描不到@Configuration注解;或者使用另一种更直接的方式,我们不通过ClassPathXmlApplicationContext启动Spring容器,而是直接通过AnnotationConfigApplicationContext加载JavaConfig类启动Spring容器。
基于BeanDefinition进行装配
这种方式前面我们在介绍BeanFactory时已经介绍过,我们这里将代码再次贴出来:
package com.gacfox.demo;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
/**
* @author gacfox
*/
public class MyToolManager implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableListableBeanFactory;
// 创建ToolConfig的BeanDefinition
BeanDefinition toolConfigDef = BeanDefinitionBuilder
.rootBeanDefinition(ToolConfig.class)
.addPropertyValue("mode", "promiscuous")
.getBeanDefinition();
// 注册toolConfig
beanFactory.registerBeanDefinition("toolConfig", toolConfigDef);
// 创建myTool的BeanDefinition
BeanDefinition myToolDef = BeanDefinitionBuilder
.rootBeanDefinition(MyTool.class)
.addPropertyReference("toolConfig", "toolConfig")
.getBeanDefinition();
// 注册myTool
beanFactory.registerBeanDefinition("myTool", myToolDef);
}
}
学习了上面XML方式、JavaConfig等方式装配Bean,我们应该已经可以看懂这段代码了。其实之前学过的声明式装配代码,最终都会被Spring抽象为BeanDefinition对象,加载到BeanFactory中,这种使用代码动态的Bean注册方式常用于底层框架的编写。
FactoryBean接口
FactoryBean顾名思义就是一个Bean工厂,如果我们的Bean创建逻辑十分复杂,或者有其它原因导致不适合直接声明式装配,此时可以使用FactoryBean间接实现Bean装配。
使用FactoryBean接口
下面是一个使用FactoryBean接口的例子。
package com.gacfox.demo.demoboot;
import org.springframework.beans.factory.FactoryBean;
public class DemoComponentFactoryBean implements FactoryBean<DemoComponent> {
@Override
public DemoComponent getObject() throws Exception {
DemoComponent demoComponent = new DemoComponent();
demoComponent.setName("Tom");
return demoComponent;
}
@Override
public Class<?> getObjectType() {
return DemoComponent.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
package com.gacfox.demo.demoboot;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DemoConfiguration {
@Bean
public DemoComponentFactoryBean demoComponentFactoryBean() {
return new DemoComponentFactoryBean();
}
}
package com.gacfox.demo.demoboot;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@Autowired
private DemoComponent demoComponent;
@GetMapping("/")
public void index() {
System.out.println(demoComponent);
}
}
上面代码中,我们的DemoComponentFactoryBean类实现了FactoryBean接口,它有3个方法分别返回具体的Bean、Bean类型和是否为单例Bean。DemoConfiguration中我们使用@Bean注册了FactoryBean,Spring框架会自动识别并处理,最终注册到Spring容器的就是FactoryBean中创建的Bean。
使用FactoryBean接口创建代理Bean
使用FactoryBean能够很容易实现基于接口的代理Bean创建,这样表述可能比较抽象,我们直接看下面例子。
package com.gacfox.demo.demoboot;
public interface IClient {
String getData();
}
package com.gacfox.demo.demoboot;
import org.springframework.beans.factory.FactoryBean;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ClientFactoryBean implements FactoryBean<IClient> {
@Override
public boolean isSingleton() {
return true;
}
@Override
public IClient getObject() throws Exception {
return (IClient) Proxy.newProxyInstance(
this.getClass().getClassLoader(),
new Class[]{IClient.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("IClient对象的getData()方法被调用了");
return "data1";
}
}
);
}
@Override
public Class<?> getObjectType() {
return IClient.class;
}
}
package com.gacfox.demo.demoboot;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DemoConfiguration {
@Bean
public ClientFactoryBean demoComponentFactoryBean() {
return new ClientFactoryBean();
}
}
package com.gacfox.demo.demoboot;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@Autowired
private IClient iClient;
@GetMapping("/")
public String index() {
return iClient.getData();
}
}
上面例子中,我们定义了一个IClient,不过实际上我们没有真的编写该接口的实现类,而是在FactoryBean中使用JDK的代理工具Proxy.newProxyInstance()生成了一个该接口的代理实现,并通过FactoryBean间接注册到了Spring容器中,此时我们的IndexController中注入的IClient对象实际上就是一个动态生成的代理对象,调用该代理对象具体执行的逻辑其实在创建代理时的InvocationHandler对象中。
这种写法在底层框架的编写中很常见,例如很多RPC框架就会基于用户自定义的接口生成代理对象,来实现RPC客户端和服务端,而不必要求用户具体编写RPC调用的代码。上面我们使用的是JDK的代理,除此之外我们也可以使用CGLIB等方式,具体写法和上面除了创建代理对象使用的API不同,注册到Spring的方法是相同的,都是通过FactoryBean来实现,这里就不多介绍了。