Spring 扩展点实践:整合 MyBATIS
在上一篇文章 Spring 扩展点概览及实践 中介绍了 Spring 内部存在的扩展点。学以致用,现在来分析一下 Spring 与 MyBATIS 的整合流程。
示例程序
为了方便分析源码,先根据官方文档 mybatis-spring – MyBatis-Spring | Getting Started 搭建起一个简单实例。
数据库方面,直接使用功能了 MySQL 示例数据库: MySQL : Employees Sample Database,需要的话,自行下载。
package com.diguage.truman.mybatis;
import com.mysql.cj.jdbc.Driver;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.session.Configuration;
import org.junit.jupiter.api.Test;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
/**
* @author D瓜哥, https://www.diguage.com/
* @since 2020-05-29 17:11
*/
public class MybatisTest {
@Test
public void test() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Config.class);
context.refresh();
EmployeesMapper employeesMapper = context.getBean(EmployeesMapper.class);
Employees employees = employeesMapper.getById(10001);
System.out.println(employees);
}
@org.springframework.context.annotation.Configuration
@MapperScan(basePackages = "com.diguage.truman.mybatis")
public static class Config {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.setDriverClassName(Driver.class.getName());
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/employees?useUnicode=true&characterEncoding=utf-8&autoReconnectForPools=true&autoReconnect=true");
return dataSource;
}
@Bean
public SqlSessionFactoryBean sqlSessionFactory(@Autowired DataSource dataSource) {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
Configuration configuration = new Configuration();
configuration.setMapUnderscoreToCamelCase(true);
factoryBean.setConfiguration(configuration);
return factoryBean;
}
}
}
package com.diguage.truman.mybatis;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* @author D瓜哥, https://www.diguage.com/
* @since 2020-05-29 17:23
*/
public interface EmployeesMapper {
@Select("SELECT * FROM employees WHERE emp_no = #{id}")
Employees getById(@Param("id") Integer id);
}
package com.diguage.truman.mybatis;
import java.util.Date;
/**
* @author D瓜哥, https://www.diguage.com/
* @since 2020-05-29 17:24
*/
public class Employees {
Integer empNo;
Date birthDate;
String firstName;
String lastName;
String gender;
Date hireDate;
@Override
public String toString() {
return "Employees{" +
"empNo=" + empNo +
", birthDate=" + birthDate +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", gender='" + gender + '\'' +
", hireDate=" + hireDate +
'}';
}
}
整个实例代码中,只有 @MapperScan(basePackages = "com.diguage.truman.mybatis")
这个注解和 MyBATIS 的配置相关,我们就从这里开始吧。
@MapperScan
处理
D瓜哥在 Spring 扩展点概览及实践:BeanDefinitionRegistryPostProcessor 中已经指出 ConfigurationClassPostProcessor
负责处理 @Configuration
注解。所以,可以直接去看这个类的代码。
ConfigurationClassPostProcessor
的处理流程都是在 processConfigBeanDefinitions(BeanDefinitionRegistry registry)
方法中完成的。在这个方法中,可以看到如下代码:
ConfigurationClassPostProcessor#processConfigBeanDefinitions
// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
parser.parse(candidates);
在 parser.parse(candidates);
这行代码打一个断点,然后一步一步跟下去,就到了 ConfigurationClassParser
的 doProcessConfigurationClass
方法里:
ConfigurationClassParser#doProcessConfigurationClass
/**
* Apply processing and build a complete {@link ConfigurationClass} by reading the
* annotations, members and methods from the source class. This method can be called
* multiple times as relevant sources are discovered.
* @param configClass the configuration class being build
* @param sourceClass a source class
* @return the superclass, or {@code null} if none found or previously processed
*/
@Nullable
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
throws IOException {
//...此处省去 N 行代码
// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
请注意这里的 getImports(sourceClass)
,我们看一下这个方法:
/**
* Returns {@code @Import} class, considering all meta-annotations.
*/
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<>();
Set<SourceClass> visited = new LinkedHashSet<>();
collectImports(sourceClass, imports, visited);
return imports;
}
/**
* Recursively collect all declared {@code @Import} values. Unlike most
* meta-annotations it is valid to have several {@code @Import}s declared with
* different values; the usual process of returning values from the first
* meta-annotation on a class is not sufficient.
* <p>For example, it is common for a {@code @Configuration} class to declare direct
* {@code @Import}s in addition to meta-imports originating from an {@code @Enable}
* annotation.
* @param sourceClass the class to search
* @param imports the imports collected so far
* @param visited used to track visited classes to prevent infinite recursion
* @throws IOException if there is any problem reading metadata from the named class
*/
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
throws IOException {
if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}
在 String annName = annotation.getMetadata().getClassName();
这行代码打断点,然后调试,注意观察 annName
变量的值,相信肯定可以看到 org.mybatis.spring.annotation.MapperScan
,接着就可以看到,通过 sourceClass.getAnnotationAttributes(Import.class.getName(), "value")
解析 @Import
注解,把其中的 org.mybatis.spring.annotation.MapperScannerRegistrar
的相关信息(被封装成了 SourceClass
对象)加入到了 imports
变量中。
下面看一下是如何处理 MapperScannerRegistrar
的。
MapperScannerRegistrar
我们接着看 processImports
方法:
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
boolean checkForCircularImports) {
//...此处省去 N 行代码
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// 很明显,会进入到这个分支
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
// 创建一个实例,然后加入到 configClass 中
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
//...此处省去 N 行代码
}
接着,回到 processConfigBeanDefinitions
方法:
ConfigurationClassPostProcessor#processConfigBeanDefinitions
parser.parse(candidates);
parser.validate();
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
进入 this.reader.loadBeanDefinitions(configClasses);
方法:
ConfigurationClassBeanDefinitionReader#loadBeanDefinitions
/**
* Read {@code configurationModel}, registering bean definitions
* with the registry based on its contents.
*/
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) {
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}
}
/**
* Read a particular {@link ConfigurationClass}, registering bean definitions
* for the class itself and all of its {@link Bean} methods.
*/
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
registrars.forEach((registrar, metadata) ->
registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator));
}
到这里就调用到了 MapperScannerRegistrar
的 registerBeanDefinitions
方法:
MapperScannerRegistrar#registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry)
/**
* {@inheritDoc}
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes mapperScanAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
generateBaseBeanName(importingClassMetadata, 0));
}
}
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
// 注意这行代码:
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
if (!Annotation.class.equals(annotationClass)) {
builder.addPropertyValue("annotationClass", annotationClass);
}
Class<?> markerInterface = annoAttrs.getClass("markerInterface");
if (!Class.class.equals(markerInterface)) {
builder.addPropertyValue("markerInterface", markerInterface);
}
Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
if (!BeanNameGenerator.class.equals(generatorClass)) {
builder.addPropertyValue("nameGenerator", BeanUtils.instantiateClass(generatorClass));
}
Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
}
String sqlSessionTemplateRef = annoAttrs.getString("sqlSessionTemplateRef");
if (StringUtils.hasText(sqlSessionTemplateRef)) {
builder.addPropertyValue("sqlSessionTemplateBeanName", annoAttrs.getString("sqlSessionTemplateRef"));
}
String sqlSessionFactoryRef = annoAttrs.getString("sqlSessionFactoryRef");
if (StringUtils.hasText(sqlSessionFactoryRef)) {
builder.addPropertyValue("sqlSessionFactoryBeanName", annoAttrs.getString("sqlSessionFactoryRef"));
}
List<String> basePackages = new ArrayList<>();
basePackages.addAll(
Arrays.stream(annoAttrs.getStringArray("value")).filter(StringUtils::hasText).collect(Collectors.toList()));
basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("basePackages")).filter(StringUtils::hasText)
.collect(Collectors.toList()));
basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName)
.collect(Collectors.toList()));
if (basePackages.isEmpty()) {
basePackages.add(getDefaultBasePackage(annoMeta));
}
String lazyInitialization = annoAttrs.getString("lazyInitialization");
if (StringUtils.hasText(lazyInitialization)) {
builder.addPropertyValue("lazyInitialization", lazyInitialization);
}
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
其实只干了一件事情,就是在想容器中注册了一个类为 MapperScannerConfigurer
的 BeanDefinition
,在创建过程中,还把 @MapperScan
注解中的属性给添加到了 BeanDefinition
属性中。下面,来看看 MapperScannerConfigurer
是何方神圣。
MapperScannerConfigurer
先看一下 MapperScannerConfigurer
的类型定义:
public class MapperScannerConfigurer
implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
结合上一篇文章 Spring 扩展点概览及实践:BeanDefinitionRegistryPostProcessor 中的介绍,可以知道 BeanDefinitionRegistryPostProcessor
也是 Spring 生命周期中的一环,将其注册到容器中,就可以通过对 postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
来实现注册自定义 BeanDefinition
的功能。
来看看 postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
的定义:
MapperScannerConfigurer#postProcessBeanDefinitionRegistry
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(lazyInitialization)) {
scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
}
scanner.registerFilters();
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
代码已经非常明确了,就是注册了一个 ClassPathMapperScanner
,同事调用了 scanner.scan
方法。下面,来看一下 ClassPathMapperScanner
。
ClassPathMapperScanner
老规矩,先看看 ClassPathMapperScanner
的定义:
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
//...此处省去 N 行代码
private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class;
public ClassPathMapperScanner(BeanDefinitionRegistry registry) {
super(registry, false);
}
从这里可以看出,ClassPathMapperScanner
就是一个 ClassPathBeanDefinitionScanner
,根据类名可以得知,扫描 class path
并生成 BeanDefinition
。来看一下 scan(String… basePackages)
ClassPathBeanDefinitionScanner#scan
/**
* Perform a scan within the specified base packages.
* @param basePackages the packages to check for annotated classes
* @return number of beans registered
*/
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
doScan(basePackages);
// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
这里把实际扫描工作委托给了 doScan(basePackages)
方法,而这个方法被 ClassPathMapperScanner
重写了,来看一下它的实现:
ClassPathMapperScanner#doScan
/**
* Calls the parent search that will search and register all the candidates. Then the registered objects are post
* processed to set them as MapperFactoryBeans
*/
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
实际的扫描工作还是由父类 super.doScan(basePackages)
完成,只是又对扫描结果做了进一步处理: processBeanDefinitions(beanDefinitions)
。
ClassPathMapperScanner#processBeanDefinitions
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
String beanClassName = definition.getBeanClassName();
LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
+ "' mapperInterface");
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
// 注意这行代码
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
// 注意这行代码
definition.setBeanClass(this.mapperFactoryBeanClass);
definition.getPropertyValues().add("addToConfig", this.addToConfig);
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory",
new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
LOGGER.warn(
() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate",
new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
LOGGER.warn(
() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
definition.setLazyInit(lazyInitialization);
}
}
这里特别需要注意的是 definition.setBeanClass(this.mapperFactoryBeanClass);
这行代码。为什么把扫描出来的 Mapper
的 Bean Class
给设置成 mapperFactoryBeanClass
呢?通过上面的 ClassPathMapperScanner
类型定义可以知道,mapperFactoryBeanClass
就是 MapperFactoryBean
。
另外,还有一点值得思考,扫描出来的是接口,怎么生成对应的实例呢?带着这两个问题,来看一下 MapperFactoryBean
。
MapperFactoryBean
来看一下 MapperFactoryBean
的类型定义:
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
private Class<T> mapperInterface;
private boolean addToConfig = true;
public MapperFactoryBean() {
// intentionally empty
}
public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
/**
* {@inheritDoc}
*/
@Override
protected void checkDaoConfig() {
super.checkDaoConfig();
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
可以看出 MapperFactoryBean
是一个 FactoryBean
,上一篇文章 Spring 扩展点概览及实践:FactoryBean 中提到,FactoryBean
就是专门生产 Bean 的工厂。
再看构造函数 public MapperFactoryBean(Class<T> mapperInterface)
,结合上一个片段代码中注意的地方可以看出,从 Class Path
扫描出来的 BeanDefinition
,把扫描出来的接口设置为构造函数参数 definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
然后通过实例化 FactoryBean
,然后调用 getObject()
就可以获得接口对应的实例对象。
实例化对象的过程是由 MyBATIS 完成的,以后单独开篇来介绍,这里不再多做介绍。
还有个疑问,MyBATIS 是怎么知道 Mapper 接口信息呢?这个问题就要看 checkDaoConfig()
方法了,单步调试代码可以知道父类 DaoSupport#afterPropertiesSet
调用的,在这个方法中,把 Mapper 接口信息条件到了 MyBATIS 中 configuration.addMapper(this.mapperInterface)
。
自此,MyBATIS 和 Spring 的整个流程就全部介绍完毕了。下面做个小节。
小节
本文从源码角度,深入绍了 MyBATIS 和 Spring 整合过程。整个过程中,用到了 Spring 的如下扩展点:
@Import
MapperScannerRegistrar
-ImportBeanDefinitionRegistrar
MapperScannerConfigurer
-BeanDefinitionRegistryPostProcessor
ClassPathMapperScanner
-ClassPathBeanDefinitionScanner
MapperFactoryBean
-FactoryBean
InitializingBean
可见,和 Spring 整合并不是只靠一个扩展点就可以完成的,需要多个扩展点多方配合才能更好地完成整合过程。
下一篇文章中,D瓜哥来介绍一下 Apache Dubbo 和 Spring 的整合过程。