Bean装配
Spring框架中,我们通过声明方式装配SpringBean,声明的内容包括SpringBean的类全名、字段属性、和其他SpringBean的依赖关系、生命周期回调函数等。具体来说,我们可以使用以下几种方式编写装配声明:
- XML配置文件方式
- JavaConfig方式
- 配置包扫描并使用注解方式
在Spring的早期版本中,XML是唯一支持的方式,不过后来随着Java版本的更新,引入了注解功能后,Spring也随之支持了注解和JavaConfig方式,并开始逐渐替代繁琐的XML方式,以上方式也可以结合使用,实现更加灵活的配置方式,因此这几种方式我们都需要熟练掌握。
基于XML进行Bean装配
引入XML Schema
在编写SpringBean声明之前,Spring的XML配置文件需要引入Schema,使用不同的功能可能需要加载不同的Schema文件。下面例子中,我们引入了最基础的两个Schema,beans和context。如果没有正确的引入Schema,我们的IDE智能提示不会起作用,Spring框架也无法正确启动。
<?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"
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">
</beans>
下面是Spring中最常用的Schema以及其引入方式。
| 命名空间前缀 | xmlns 声明 | 常用 schemaLocation | 主要用途 |
|---|---|---|---|
| beans | xmlns="http://www.springframework.org/schema/beans" |
http://www.springframework.org/schema/beans/spring-beans.xsd |
定义 bean、依赖注入(最核心) |
| context | xmlns:context="http://www.springframework.org/schema/context" |
http://www.springframework.org/schema/context/spring-context.xsd |
组件扫描、注解配置、PropertyPlaceholder 等 |
| aop | xmlns:aop="http://www.springframework.org/schema/aop" |
http://www.springframework.org/schema/aop/spring-aop.xsd |
面向切面编程(<aop:config>、<aop:aspect> 等) |
| tx | xmlns:tx="http://www.springframework.org/schema/tx" |
http://www.springframework.org/schema/tx/spring-tx.xsd |
声明式事务(<tx:annotation-driven>、<tx:advice>) |
| util | xmlns:util="http://www.springframework.org/schema/util" |
http://www.springframework.org/schema/util/spring-util.xsd |
工具类配置(如 <util:list>、<util:map>、<util:constant>、<util:properties>) |
| mvc (Web) | xmlns:mvc="http://www.springframework.org/schema/mvc" |
http://www.springframework.org/schema/mvc/spring-mvc.xsd |
Spring MVC 配置(<mvc:annotation-driven>、资源映射、拦截器等) |
当然,我们完全没必要记住这些繁琐的配置,具体使用到哪个模块时,查询对应文档即可。
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中的基本数据类型及封装类,都可以直接使用字面值方式进行装配。
<property name="count" value="100" /> <!-- int 或 Integer -->
<property name="price" value="99.99" /> <!-- double 或 Double -->
<property name="flag" value="true" /> <!-- boolean 或 Boolean -->
除此之外,如果字符串值中包含特殊字符,需要使用XML的CDATA语法。
<property name="expression" value="<![CDATA[a > b && c < d]]>" />
SpringBean类型引用注入
如果SpringBean中的一个字段是另一个Bean,需要使用ref属性。
<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可用,前两种也是最常用的,我们必须熟练掌握。
下面代码中,我们声明toolConfig这个SpringBean为原型模式,如果我们用applicationContext.getBean()方法进行获取,我们会发现每次获取的对象都是新创建的。
<bean id="toolConfig" class="com.gacfox.demo.ToolConfig" scope="prototype">
<property name="mode" value="promiscuous" />
</bean>
通过动态代理注入依赖SpringBean
虽然我们现在已经基本掌握了Spring的装配机制,但是先别高兴的太早,考虑这样一种场景:单例模式的BeanA依赖于原型模式的BeanB,我们的BeanA会在应用上下文启动时创建,而此时还没有BeanB对象,那么注入显然不会成功。但从业务逻辑上来说,这肯定是合理的诉求!
此时需要使用动态代理,将BeanB的代理注入给BeanA,至到其取用BeanB时再真正创建BeanB,好消息是我们不需要手写这个逻辑,Spring已经配置支持了,对应的XML配置方法如下。
<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 lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Data
@Component
public class MyClient {
@Value("${host}")
private String host;
@Value("${port}")
private String port;
@Value("${password}")
private String password;
}
注意:此种方式也必须在XML配置文件中开启context:property-placeholder功能。
基于注解自动装配
Spring框架支持自动装配,也就是不需要声明自动根据字段类型注入SpringBean,下面是一个例子。
package com.gacfox.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class DemoService {
private final MyClient myClient;
@Autowired
public DemoService(MyClient myClient) {
this.myClient = myClient;
}
public void doSomething() {
log.info("myClient: {} {} {}", myClient.getHost(), myClient.getPort(), myClient.getPassword());
}
}
@Autowired注解声明Bean中该字段需要自动装配,我们这里基于构造函数进行了装配,它默认是使用by-type进行依赖注入的,也就是基于类型来注入。不过这种by-type自动装配时可能遇到一个问题,如果同一个Java类型有多个Bean存在,这时就存在冲突了。此时,我们可以用@Qualifier注解再指定Bean名。
package com.gacfox.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class DemoService {
private final MyClient myClient;
@Autowired
public DemoService(@Qualifier("myClient") MyClient myClient) {
this.myClient = myClient;
}
public void doSomething() {
log.info("myClient: {} {} {}", myClient.getHost(), myClient.getPort(), myClient.getPassword());
}
}
注意:
- 上面介绍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;
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;
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;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DemoConfiguration {
@Bean
public DemoComponentFactoryBean demoComponent() {
return new DemoComponentFactoryBean();
}
}
package com.gacfox.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
@Slf4j
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
DemoComponent demoComponent = context.getBean("demoComponent", DemoComponent.class);
log.info(demoComponent.getName());
}
}
上面代码中,我们的DemoComponentFactoryBean类实现了FactoryBean接口,它有3个方法分别返回具体的Bean、Bean类型和是否为单例Bean。DemoConfiguration中我们使用@Bean注册了FactoryBean,不过我们需要将方法名指定为demoComponent,它会作为具体的Bean名,Spring框架会自动识别并处理,最终注册到Spring容器的就是FactoryBean中创建的Bean。
使用FactoryBean接口创建代理Bean
使用FactoryBean能够很容易实现基于接口的代理Bean创建,这样表述可能比较抽象,我们直接看下面例子。
package com.gacfox.demo;
public interface IClient {
String getData();
}
package com.gacfox.demo;
import org.springframework.beans.factory.FactoryBean;
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},
(proxy, method, args) -> {
System.out.println("IClient对象的getData()方法被调用了");
return "data1";
}
);
}
@Override
public Class<?> getObjectType() {
return IClient.class;
}
}
package com.gacfox.demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DemoConfiguration {
@Bean
public ClientFactoryBean iClient() {
return new ClientFactoryBean();
}
}
package com.gacfox.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
@Slf4j
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
IClient iClient = context.getBean("iClient", IClient.class);
log.info(iClient.getData());
}
}
上面例子中,我们定义了接口IClient,不过实际上我们没有真的编写该接口的实现类,而是在FactoryBean中使用JDK的代理工具Proxy.newProxyInstance()生成了一个该接口的代理实现,并通过FactoryBean间接注册到了Spring容器中,此时我们在main()中获取到的IClient对象实际上就是动态生成的代理对象,调用该代理对象具体执行的逻辑其实在创建代理时的InvocationHandler对象中。
这种写法在底层框架的编写中很常见,例如很多RPC框架就会基于用户自定义的接口生成代理对象,来实现RPC客户端和服务端,而不必要求用户具体编写RPC调用的代码。上面我们使用的是JDK的代理,除此之外我们也可以使用CGLIB等方式,具体写法和上面除了创建代理对象使用的API不同,注册到Spring的原理是相同的,都是通过FactoryBean来实现,这里就不多介绍了。