【Spring】简述@Configuration配置类注册BeanDefinition到Spring容器的过程
概述
本文以SpringBoot应用为基础,尝试分析基于注解@Configuration的配置类是如何向Spring容器注册BeanDefinition的过程
其中主要分析了 ConfigurationClassPostProcessor 这个BeanDefinitionRegistryPostProcessor 即Bean定义注册后置处理器,在Spring启动过程中对@Configuration配置类的处理,主要体现在 解析并发现所有配置类,处理配置类的相关逻辑(如配置类上的@ComponentScan、@Import、@Bean注解等),注册其中的BeanDefinition
SpringBoot版本:2.0.9.RELEASE
Spring版本:5.0.13.RELEASE
ConfigurationClassPostProcessor如何被引入
首先看一下ConfigurationClassPostProcessor的类继承关系

从红框中可以看出ConfigurationClassPostProcessor是BeanDefinitionRegistryPostProcessor接口的实现类,即是一个Bean定义注册的后置处理器,会在Spring容器启动时被调用,具体时机为
// 调用链
AbstractApplicationContext.refresh()
=> invokeBeanFactoryPostProcessors()
=> PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors()
invokeBeanFactoryPostProcessors()会先调用所有的BeanDefinitionRegistryPostProcessor之后,再调用所有的BeanFactoryPostProcessor
ConfigurationClassPostProcessor又是如何被引入Spring的呢??
SpringBoot应用会在ApplicationContext应用上下文被创建的构造函数中new AnnotatedBeanDefinitionReader这个用于注册基于注解的BeanDefinition的Reader,在其构造中又会调用AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry)使用工具类向Spring容器中注册一些所谓的注解配置处理器,其中就包含ConfigurationClassPostProcessor
// ConfigurationClassPostProcessor被注册
AnnotationConfigServletWebServerApplicationContext构造
=> new AnnotatedBeanDefinitionReader(registry)
=> AnnotationConfigUtils.registerAnnotationConfigProcessors(registry)
=> 注册ConfigurationClassPostProcessor到Spring容器
ConfigurationClassPostProcessor处理过程简述
首先,ConfigurationClassPostProcessor后置处理器的处理入口为postProcessBeanDefinitionRegistry()方法。其主要使用了ConfigurationClassParser配置类解析器解析@Configuration配置类上的诸如@ComponentScan、@Import、@Bean等注解,并尝试发现所有的配置类;还使用了ConfigurationClassBeanDefinitionReader注册所发现的所有配置类中的所有Bean定义;结束执行的条件是所有配置类都被发现和处理,相应的bean定义注册到容器
大致流程如下:
1、通过BeanDefinitionRegistry查找当前Spring容器中所有BeanDefinition
2、通过ConfigurationClassUtils.checkConfigurationClassCandidate() 检查BeanDefinition是否为 “完全配置类” 或 “简化配置类”,并对配置类做标记,放入集合待后续处理
Spring配置类的分类可以 参考
3、通过 ConfigurationClassParser解析器 parse解析配置类集合,尝试通过它们找到其它配置类
4、使用 ConfigurationClassBeanDefinitionReader 注册通过所发现的配置类中找到的所有beanDefinition
5、处理完一轮配置类后,查看BeanDefinitionRegistry中是否存在新加载的且还未被处理过的 “完全配置类” 或 “简化配置类”,有的话继续上面步骤
其中第3、4步后面重点分析
ConfigurationClassParser#parse():解析构建配置类
对于SpringBoot应用来说,参与解析的种子配置文件即为SpringBoot的Application启动类
解析构建配置类流程
通过ConfigurationClassParser解析器parse解析配置类集合,尝试通过它们找到其它配置类
循环解析所有配置类 ConfigurationClassParser#processConfigurationClass()
根据@Conditional的ConfigurationPhase.PARSE_CONFIGURATION阶段条件判断是否跳过配置类
注意:有些@Conditional是在当前这个PARSE_CONFIGURATION解析配置阶段使用的,有些是在REGISTER_BEAN注册beanDefinition阶段使用的
【重点】调用ConfigurationClassParser#doProcessConfigurationClass()循环解析配置类,直到不存在未处理过的父类
- 1、处理配置类的成员内部类: 检查其是否为“完全/简化配置类”,是则对其继续分析处理并将其放入分析器的属性configurationClasses
- 2、处理@PropertySource: 将找到的PropertySource添加到environment的PropertySource集合
- 3、处理@ComponentScan: 扫描到的@Component类BeanDefinition就直接注册到Spring容器;如果组件为配置类,继续分析处理并将其放入分析器的属性configurationClasses
- 4、处理@Import:
- (1)处理ImportSelector: 如果是DeferredImportSelector,如SpringBoot的自动配置导入,添加到deferredImportSelectors,延迟进行processImports();其它通过ImportSelector找到的类,继续调用processImports(),要么是@Configuration配置类继续解析,要么是普通组件导入Spring容器
- (2)处理ImportBeanDefinitionRegistrar: 调用当前配置类的addImportBeanDefinitionRegistrar(),后面委托它注册其它bean定义
- (3)其它Import:调用processConfigurationClass()继续解析,最终要么是配置类放入configurationClasses,要么是普通组件导入Spring容器
- 5、处理@ImportResource: 添加到配置类的importedResources集合,后续ConfigurationClassBeanDefinitionReader#loadBeanDefinitions()时再使用这些导入的BeanDefinitionReader读取Resource中的bean定义并注册
- 6、处理@Bean: 获取所有@Bean方法,并添加到配置类的beanMethods集合
- 7、处理配置类接口上的default methods
- 8、检查是否有未处理的父类: 如果配置类有父类,且其不在解析器维护的knownSuperclasses中,对其调用doProcessConfigurationClass()重复如上检查,直到不再有父类或父类在knownSuperclasses中已存在
processDeferredImportSelectors():处理推迟的ImportSelector集合,其实就是延迟调用了processImports()
SpringBoot的自动配置类就是被DeferredImportSelector推迟导入的
解析构建配置类源码分析
ConfigurationClassParser#processConfigurationClass()
包含了处理单个配置类的大体流程,先根据ConfigurationPhase.PARSE_CONFIGURATION解析配置阶段的@Conditional条件判断当前配置类是否应该解析,之后调用ConfigurationClassParser#doProcessConfigurationClass()循环解析配置类,直到不存在未处理过的父类
/**
* 解析单个配置类
* 解析的最后会将当前配置类放到configurationClasses
*/
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
/**
* 根据@Conditional条件判断是否跳过配置类
* 注意:当前这个PARSE_CONFIGURATION解析配置阶段只会使用这个阶段的@Conditional条件,有些REGISTER_BEAN注册beanDefinition阶段的条件不会在此时使用
*/
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}
ConfigurationClass existingClass = this.configurationClasses.get(configClass);
// 如果configClass在已经分析处理的配置类记录中已存在
if (existingClass != null) {
//如果配置类是被@Import注册的,return
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
// Otherwise ignore new imported config class; existing non-imported class overrides it.
return;
}
// 否则,清除老的记录,在来一遍
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}
// Recursively process the configuration class and its superclass hierarchy.
/**
* 递归处理配置类及其超类层次结构
* 从当前配置类configClass开始向上沿着类继承结构逐层执行doProcessConfigurationClass,直到遇到的父类是由Java提供的类结束循环
*/
SourceClass sourceClass = asSourceClass(configClass);
/**
* 循环处理配置类configClass直到sourceClass变为null,即父类为null
* doProcessConfigurationClass的返回值是其参数configClass的父类
* 如果该父类是由Java提供的类或者已经处理过,返回null
*/
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);
this.configurationClasses.put(configClass, configClass);
}
ConfigurationClassParser#doProcessConfigurationClass():真正解析配置类
通过解析配置类上的注解、内部成员类和方法构建一个完整的ConfigurationClass配置类,过程中如果发现了新的配置类可以重复调用此方法
真正解析过程中会处理成员内部类、@PropertySource、@ComponentScan、@Import、@ImportSource、@Bean方法等,流程如下:
- 1、处理配置类的成员内部类: 检查其是否为“完全/简化配置类”,是则对其继续分析处理并将其放入分析器的属性configurationClasses
- 2、处理@PropertySource: 将找到的PropertySource添加到environment的PropertySource集合
- 3、处理@ComponentScan: 扫描到的@Component类BeanDefinition就直接注册到Spring容器;如果组件为配置类,继续分析处理并将其放入分析器的属性configurationClasses
- 4、处理@Import:
- (1)处理ImportSelector: 如果是DeferredImportSelector,如SpringBoot的自动配置导入,添加到deferredImportSelectors,延迟进行processImports();其它通过ImportSelector找到的类,继续调用processImports(),要么是@Configuration配置类继续解析,要么是普通组件导入Spring容器
- (2)处理ImportBeanDefinitionRegistrar: 调用当前配置类的addImportBeanDefinitionRegistrar(),后面委托它注册其它bean定义
- (3)其它Import: 调用processConfigurationClass()继续解析,最终要么是配置类放入configurationClasses,要么是普通组件导入Spring容器
- 5、处理@ImportResource: 添加到配置类的importedResources集合,后续ConfigurationClassBeanDefinitionReader#loadBeanDefinitions()时再使用这些导入的BeanDefinitionReader读取Resource中的bean定义并注册
- 6、处理@Bean: 获取所有@Bean方法,并添加到配置类的beanMethods集合
- 7、处理配置类接口上的default methods
- 8、检查是否有未处理的父类: 如果配置类有父类,且其不在解析器维护的knownSuperclasses中,对其调用doProcessConfigurationClass()重复如上检查,直到不再有父类或父类在knownSuperclasses中已存在
/**
* 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)
throws IOException {
// Recursively process any member (nested) classes first
/**
* 1、处理配置类的成员类(配置类内嵌套定义的类)
* 内部嵌套类也可能是配置类,遍历这些成员类,检查是否为"完全/简化配置类"
* 有的话,调用processConfigurationClass()处理它们,最终将配置类放入configurationClasses集合
*/
processMemberClasses(configClass, sourceClass);
// Process any @PropertySource annotations
/**
* 2、处理 @PropertySource
* 将找到的PropertySource添加到environment的PropertySource集合
*/
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}
// Process any @ComponentScan annotations
/**
* 3、处理 @ComponentScan
* 处理用户手工添加的@ComponentScan,SpringBoot创建ApplicationContext时的ClassPathBeanDefinitionScanner是为了扫描启动类下的包
* 为的是找到满足条件的@ComponentScan,即@Component相关的组件,先扫描一下,扫描到的就注册为BeanDefinition
* 看其中是否还有配置类,有的话parse()继续分析处理,配置类添加到configurationClasses集合
*/
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
// 如果当前配置类上有@ComponentScan,且使用REGISTER_BEAN注册beanDefinition的条件判断也不跳过的话
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
// 立即扫描,扫描到的就注册为BeanDefinition,并获得扫描到的所有beanDefinition
// 在处理SpringBoot启动类上的@ComponentScan时,虽然指指定了excludeFilters,但会根据启动类所在包推测basePackage,就会扫描到SpringBoot启动类包以下的Bean并注册
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
// 检查扫描到的beanDefinition中是否有配置类,有的话parse()继续分析处理,,配置类添加到configurationClasses集合
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
// Process any @Import annotations
/**
* 4、处理 @Import
* (1)处理ImportSelector
* 如果是DeferredImportSelector,如SpringBoot的自动配置导入,添加到deferredImportSelectors,延迟进行processImports()
* 其它通过ImportSelector找到的类,继续调用processImports(),要么是@Configuration配置类继续解析,要么是普通组件导入Spring容器
* (2)处理ImportBeanDefinitionRegistrar
* 调用当前配置类的addImportBeanDefinitionRegistrar(),后面委托它注册其它bean定义
* (3)其它
* 调用processConfigurationClass()继续解析,最终要么是配置类放入configurationClasses,要么是普通组件导入Spring容器
*/
processImports(configClass, sourceClass, getImports(sourceClass), true);
// Process any @ImportResource annotations
/**
* 5、处理 @ImportResource
* 添加到配置类的importedResources集合,后续loadBeanDefinitions()加载bean定义时再让这些导入BeanDefinitionReader自行读取bean定义
*/
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}
// Process individual @Bean methods
/**
* 6、处理个别@Bean方法
* 获取所有@Bean方法,并添加到配置类的beanMethods集合
*/
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}
// Process default methods on interfaces
/**
* 7、处理配置类接口上的default methods
*/
processInterfaces(configClass, sourceClass);
// Process superclass, if any
/**
* 8、检查父类是否需要处理,如果父类需要处理返回父类,否则返回null
* 如果存在父类,且不在knownSuperclasses已经分析过的父类列表里,返回并继续分析
*/
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}
// No superclass -> processing is complete
return null;
}
ConfigurationClassBeanDefinitionReader#loadBeanDefinitions():读取配置类,基于配置信息注册BeanDefinition
读取配置类,基于配置信息注册BeanDefinition流程
在上面解析配置类的过程中,除了构建了一个完整的ConfigurationClass配置类,其实已经向BeanDefinitionRegistry中添加了一些beanDefinition了,比如在处理@ComponentScan时,扫描到的@Component相关组件就已经注册了
而ConfigurationClassBeanDefinitionReader会继续读取已经构建好的ConfigurationClass配置类中的成员变量,从而注册beanDefinition
构建好的ConfigurationClass配置类中在本阶段可用的成员变量包括:
Set<BeanMethod> beanMethods: @Bean的方法Map<String, Class<? extends BeanDefinitionReader>> importedResources:配置类上@ImportResource注解的类存入此集合,会使用BeanDefinitionReader读取Resource中的BeanDefinition并注册Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> importBeanDefinitionRegistrars:ImportBeanDefinitionRegistrar集合
通过构建好的配置类的配置信息,使用ConfigurationClassBeanDefinitionReader注册所有能够读取到的beanDefinition:
根据
ConfigurationPhase.REGISTER_BEAN阶段条件判断配置类是否需要跳过循环判断配置类以及导入配置类的类,使用ConfigurationPhase.REGISTER_BEAN阶段条件判断是否需要跳过只要配置类或导入配置类的类需要跳过即返回跳过
如果configClass.isImported(),将配置类自身注册为beanDefinition
注册配置类所有
@Bean方法为beanDefinition注册由
@ImportedResources来的beanDefinition,即通过其它类型Resource的BeanDefinitionReader读取BeanDefinition并注册,如xml格式的配置源XmlBeanDefinitionReader注册由
ImportBeanDefinitionRegistrars来的beanDefinition
读取配置类,基于配置信息注册BeanDefinition源码分析
/**
* Read a particular {@link ConfigurationClass}, registering bean definitions
* for the class itself and all of its {@link Bean} methods.
* 读取特定配置类,根据配置信息注册bean definitions
*/
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
/**
* 根据ConfigurationPhase.REGISTER_BEAN阶段条件判断配置类是否需要跳过
* 循环判断配置类以及导入配置类的类,使用ConfigurationPhase.REGISTER_BEAN阶段条件判断是否需要跳过
* 只要配置类或导入配置类的类需要跳过即返回跳过
*/
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;
}
// 1、如果当前配置类是通过内部类导入 或 @Import导入,将配置类自身注册为beanDefinition
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
// 2、注册配置类所有@Bean方法为beanDefinition
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
// 3、注册由@ImportedResources来的beanDefinition
// 即通过其它类型Resource的BeanDefinitionReader读取BeanDefinition并注册
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
// 4、注册由ImportBeanDefinitionRegistrars来的beanDefinition
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
思维导图
请放大观看

参考
Spring BeanDefinitionRegistryPostProcessor:ConfigurationClassPostProcessor
Spring 工具类 ConfigurationClassParser 分析得到配置类
【Spring】简述@Configuration配置类注册BeanDefinition到Spring容器的过程的更多相关文章
- 真懂Spring的@Configuration配置类?你可能自我感觉太良好
当大潮退去,才知道谁在裸泳.关注公众号[BAT的乌托邦]开启专栏式学习,拒绝浅尝辄止.本文 https://www.yourbatman.cn 已收录,里面一并有Spring技术栈.MyBatis.中 ...
- Spring源码解析 – @Configuration配置类及注解Bean的解析
在分析Spring 容器创建过程时,我们知道容器默认会加载一些后置处理器PostPRocessor,以AnnotationConfigApplicationContext为例,在构造函数中初始化rea ...
- spring5 源码深度解析----- 被面试官给虐懵了,竟然是因为我不懂@Configuration配置类及@Bean的原理
@Configuration注解提供了全新的bean创建方式.最初spring通过xml配置文件初始化bean并完成依赖注入工作.从spring3.0开始,在spring framework模块中提供 ...
- Spring之@Configuration配置解析
1.简单的示例: @Configuration @EnableConfigurationProperties({DemoProperties.class}) public class DemoConf ...
- Spring Boot自动配置类
http://docs.spring.io/spring-boot/docs/current/api/overview-summary.html http://docs.spring.io/sprin ...
- Spring boot添加配置类@Configuration并初始化@Bean,@Resource和@Autowired都为null
大写加黑,找了好久@Resource和@Autowired都依赖不到创建的bean的原因:@Bean的方法名即是创建的Bean名称 import org.activiti.engine.Process ...
- springboot中解决servlet乱码问题,使用配置类注册过滤器解决
8.1 使用传统的Spring提供的字符编码过滤器 在03-springboot-web中的ServletConfig中配置文件类,注册字符编码过滤器 @Bean public FilterRegis ...
- springboot @Configuration配置类里面使用@Value获取不到.yml配置文件属性的值
之前一个项目里面分为很多子工程的那种结构,要求让我改成一个项目的结构.我这边手动将代码合并之后出现下面的这种问题,然后尝试进行用各种方式解决 Error creating bean with name ...
- spring 自己创建配置类
随机推荐
- Spring Boot入门(二):获取配置文件值
本篇博客主要讲解下在Spring Boot中如何获取配置文件的值. 1. 使用yaml配置文件 Spring Boot默认生成的配置文件为application.properties,不过它也支持ya ...
- Swagger--解决日期格式显示为Unix时间戳格式 UTC格式
在swagger UI模型架构上,字段日期显示为“日期”:“2018-10-15T09:10:47.507Z”但我需要将其作为“日期”:“2018-9-26 12:18:48”. tips:以下这两种 ...
- 设计模式(四)Factory Method模式
简单来说,用Template Method模式来构建生成实例的工厂,就是Factory Method模式.在这个模式中,父类决定实例的生成方式,但不决定所要生成的具体的类,具体的处理全部交给子类负责. ...
- mysql的一些常用操作(一)
1.启动Mysql服务 net start mysql 2.进入mysql环境中,由于自己没有设置密码,直接回车进入即可(要将bin加入到环境变量path中) mysql -u root -p 3.创 ...
- C#开发命令执行驱动程序 之 控制标志的命令行参数
/// <summary> /// 在cmd窗体内执行如下: /// CtrlOrderDrierApp.exe -f -t /// 返回: /// FOO /// Show Table ...
- ios Autolayout 按比例相对布局
看到一篇讲ios storyboard 按比例相对布局的博客,挺不错的转下来了! 可到liumh.com查看. 本文记录如何在 UIStoryboard 或者 xib 中进行百分比布局,包括 View ...
- 【MySQL】MySQL数据类型
MySQL表数据存储大小说明 MySQL中规定,任何一条记录(数据表中每行数据)理论上的最大存储容量为 2^16 - 1 (Bytes) = 65535字节. MySQL数据类型思维导图 MySQL数 ...
- 第二十九章 System V共享内存
共享内存数据结构 共享内存函数 shmget int shmget(key_t key, size_t size, int shmflg); 功能: 用于创建共享内存 参数: key : 这个共享内存 ...
- USDT跑分系统开发
USDT跑分系统开发找[林生:178-7666-2415微/电],USDT跑分系统开发,USDT跑分系统解决方案,USDT跑分系统定制,小程序开发.APP.公众号等软件开发. 数字资产结算通道的特点: ...
- 设计模式之代理模式(Java)
简介 代理模式出场率真的相当的高,几乎所有框架中无一例外都用到了代理模式,所以了解一下收益还是很高的. 代理模式是什么 如果用一句话来描述代理模式: 代理模式就是为其他对象提供一种代理以控制对被代理对 ...