手写Spring:第4章-基于Cglib实现含构造函数的类实例化策略

news2024/10/1 23:48:50

文章目录

  • 一、目标:含构造函数的类实例化
  • 二、设计:含构造函数的类实例化
  • 三、实现:含构造函数的类实例化
    • 3.1 工程结构
    • 3.2 含构造函数的类实例化类图
    • 3.3 类实例化策略
      • 3.3.1 定义实例化策略接口
      • 3.3.2 JDK实例化
      • 3.3.3 Cglib实例化
    • 3.4 抽象类定义模板方法
      • 3.4.1 Bean工厂接口,新增getBean接口
      • 3.4.2 抽象Bean工厂基类,定义模板方法
      • 3.4.3 创建策略调用
  • 四、测试:含构造函数的类实例化
    • 4.1 用户Bean对象
    • 4.2 单元测试
      • 4.2.1 单元测试
      • 4.2.2 无构造函数测试
      • 4.2.3 有构造函数实例化测试
      • 4.2.4 获取构造函数信息
      • 4.2.5 Cglib实例化
  • 五、总结:含构造函数的类实例化

一、目标:含构造函数的类实例化

💡 关于 Bean 对象在含有构造函数进行实例化?

  • 之前扩充了 Bean 容器的功能,把实例化对象交给容器来统一处理,但在我们实例化对象里并没有考虑对象类是否含构造函数。
    • 也就是说如果我们去实例化一个含有构造函数的对象就要抛异常。

UserService

public class UserService {

    private String name;

    public UserService() {
    }

    public UserService(String name) {
        this.name = name;
    }
}

报错如下:

Caused by: java.lang.InstantiationException: com.lino.springframework.test.bean.UserService
    at java.lang.Class.newInstance(Class.java:427)
    at com.lino.springframework.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:17)
    ... 26 more
  • 发现这一现象的主要原因是 beanDefinition.getBeanClass().newInstance()。实例化方式并没有考虑构造函数的入参。

二、设计:含构造函数的类实例化

💡 技术设计需要考虑两部分?

  • 一个是串联流程从哪合理的把构造函数的入参信息传递给实例化操作里。
  • 另一个是怎么去实例化含有构造函数的对象。

在这里插入图片描述

  • 参考 Spring Bean 容器源码的实现方式,在 BeanFactory 中添加 Object getBean(String name, Object... args) 接口。
    • 这样就可以在获取 Bean 时把构造函数的入参信息传递进去。
  • 核心:使用什么方式来创建含有构造函数的 Bean 对象呢?
    • 一个是基于 Java 本身自带的方法 DeclaredConstructor
    • 另一个是使用 Cglib 来动态创建 Bean 对象。Cglib 是基于字节码框架 ASM 实现,所以可以直接通过 ASM 操作指令码来创建对象

三、实现:含构造函数的类实例化

3.1 工程结构

spring-step-03
|-src
    |-main
    |   |-java
    |       |-com.lino.springframework
    |           |-factory
    |           |   |-config
    |           |   |   |-BeanDefinition.java
    |           |   |   |-SingletonBeanRegistry.java
    |           |   |-support
    |           |   |   |-AbstractAutowireCapableBeanFactory.java
    |           |   |   |-AbstractBeabFactory.java
    |           |   |   |-BeanDefinitionRegistry.java
    |           |   |   |-CglibSubclassingInstantiationStrategy.java
    |           |   |   |-DefaultListableBeanFactory.java
    |           |   |   |-DefaultSingletonBeanRegistry.java
    |           |   |   |-InstantiationStrategy.java
    |           |   |   |-SimpleInstantiationStrategy.java
    |           |   |-BeanFactory.java
    |           |-BeansException.java
    |-test
        |-java
            |-com.lino.springframework.test
            |-bean
            |   |-UserService.java
            |-ApiTest.java

3.2 含构造函数的类实例化类图

在这里插入图片描述

  • 主要添加 InstantiationStrategy 实例化策略接口,以及补充相应的 getBean 入参信息,让外部调用时可以传递构造函数的入参并顺利实例化。

3.3 类实例化策略

3.3.1 定义实例化策略接口

InstantiationStrategy.java

package com.lino.springframework.factory.support;

import com.lino.springframework.BeansException;
import com.lino.springframework.factory.config.BeanDefinition;
import java.lang.reflect.Constructor;

/**
 * @description: Bean 实例化策略接口
 */
public interface InstantiationStrategy {

    /**
     * 实例化
     *
     * @param beanDefinition Bean 对象
     * @param beanName       要检索的bean名称
     * @param ctor           类信息
     * @param args           构造函数入参
     * @return 实例化后的对象
     * @throws BeansException 不能获取 Bean 对象,抛出异常
     */
    Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args) throws BeansException;
}
  • 在实例化接口 InstantiationStrategy 方法中添加必要的入参信息,包括:beanDefinition、beanName、ctor、args
  • 其中 Constructor,它是 java.lang.reflect 包下的 Constructor 类,里面包含了一些必要的类信息。
    • 有这个参数的目的就是为了拿到符合入参信息相对应的构造函数。
  • args 就是一个具体的入参信息,最终实例化时会用到。

3.3.2 JDK实例化

SimpleInstantiationStrategy.java

package com.lino.springframework.factory.support;

import com.lino.springframework.BeansException;
import com.lino.springframework.factory.config.BeanDefinition;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @description: JDK实例化策略
 */
public class SimpleInstantiationStrategy implements InstantiationStrategy {

    @Override
    public Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args) throws BeansException {
        Class clazz = beanDefinition.getBeanClass();
        try {
            if (null != ctor) {
                return clazz.getDeclaredConstructor(ctor.getParameterTypes()).newInstance(args);
            } else {
                return clazz.getDeclaredConstructor().newInstance();
            }
        } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
            throw new BeansException("Failed to instantiate [" + clazz.getName() + "]", e);
        }
    }
}
  • 首先通过 beanDefinition 获取 Class 信息,这个 Class 信息是在 Bean 定义的时候传递进去的。
  • 接下来判断 ctor 是否为空,如果为空则是无构造函数实例化,否则就是需要有构造函数的实例化。
  • 这里我们重点关注有构造函数的实例化,实例化方式为 clazz.getDeclaredConstructor(ctor.getParameterTypes()).newInstance(args),把入参信息传递给 newInstance 进行实例化。

3.3.3 Cglib实例化

CglibSubclassingInstantiationStrategy.java

package com.lino.springframework.factory.support;

import com.lino.springframework.BeansException;
import com.lino.springframework.factory.config.BeanDefinition;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.NoOp;
import java.lang.reflect.Constructor;

/**
 * @description: Cglib 实例化策略
 */
public class CglibSubclassingInstantiationStrategy implements InstantiationStrategy {

    @Override
    public Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args) throws BeansException {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(beanDefinition.getBeanClass());
        enhancer.setCallback(new NoOp() {
            @Override
            public int hashCode() {
                return super.hashCode();
            }
        });
        if (null == ctor) {
            return enhancer.create();
        }
        return enhancer.create(ctor.getParameterTypes(), args);
    }
}
  • 其实 Cglib 创建有构造函数的 Bean 也非常方便。

3.4 抽象类定义模板方法

3.4.1 Bean工厂接口,新增getBean接口

BeanFactory.java

package com.lino.springframework.factory;

import com.lino.springframework.BeansException;

/**
 * @description: 定义 Bean 工厂接口
 */
public interface BeanFactory {

    /**
     * 返回 Bean 的实例对象
     *
     * @param name 要检索的bean的名称
     * @return 实例化的 Bean 对象
     * @throws BeansException 不能获取 Bean 对象,抛出异常
     */
    Object getBean(String name) throws BeansException;

    /**
     * 返回含构造函数的 Bean 实例对象
     *
     * @param name 要检索的bean的名称
     * @param args 构造函数入参
     * @return 实例化的 Bean 对象
     * @throws BeansException 不能获取 Bean 对象,抛出异常
     */
    Object getBean(String name, Object... args) throws BeansException;
}
  • BeanFactory 中重载了一个含有入参信息 argsgetBean 方法,这样就可以传递入参给构造函数实例化了。

3.4.2 抽象Bean工厂基类,定义模板方法

AbstractBeanFactory.java

package com.lino.springframework.factory.support;

import com.lino.springframework.BeansException;
import com.lino.springframework.factory.BeanFactory;
import com.lino.springframework.factory.config.BeanDefinition;

/**
 * @description: 抽象的 Bean 工厂基类,定义模板方法
 * @author: lingjian
 * @createDate: 2022/11/22 14:34
 */
public abstract class AbstractBeanFactory extends DefaultSingletonBeanRegistry implements BeanFactory {

    @Override
    public Object getBean(String name) throws BeansException {
        return doGetBean(name, null);
    }

    @Override
    public Object getBean(String name, Object... args) throws BeansException {
        return doGetBean(name, args);
    }

    protected <T> T doGetBean(final String name, final Object[] args) {
        Object bean = getSingleton(name);
        if (bean != null) {
            return (T) bean;
        }
        BeanDefinition beanDefinition = getBeanDefinition(name);
        return (T) createBean(name, beanDefinition, args);
    }

    /**
     * 获取 Bean 对象
     *
     * @param beanName 要检索的bean的名称
     * @return Bean 对象
     */
    protected abstract BeanDefinition getBeanDefinition(String beanName);

    /**
     * 创建Bean对象
     *
     * @param beanName       要检索的bean的名称
     * @param beanDefinition Bean对象
     * @param args           构造函数入参
     * @return 实例化的Bean对象
     */
    protected abstract Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args);
}
  • 添加含构造参数的 getBean 方法。

3.4.3 创建策略调用

AbstractAutowireCapableBeanFactory.java

package com.lino.springframework.factory.support;

import com.lino.springframework.BeansException;
import com.lino.springframework.factory.config.BeanDefinition;
import java.lang.reflect.Constructor;

/**
 * @description: 实现默认bean创建的抽象bean工厂超类
 */
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory {

    private InstantiationStrategy instantiationStrategy = new CglibSubclassingInstantiationStrategy();

    @Override
    protected Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args) {
        Object bean = null;
        try {
            bean = createBeanInstance(beanDefinition, beanName, args);
        } catch (Exception e) {
            throw new BeansException("Instantiation of bean failed", e);
        }
        registerSingletonBean(beanName, bean);
        return bean;
    }

    protected Object createBeanInstance(BeanDefinition beanDefinition, String beanName, Object[] args) {
        Constructor constructorToUse = null;
        Class<?> beanClass = beanDefinition.getBeanClass();
        Constructor<?>[] declaredConstructors = beanClass.getDeclaredConstructors();
        for (Constructor ctor : declaredConstructors) {
            if (null != args && ctor.getParameterTypes().length == args.length) {
                constructorToUse = ctor;
                break;
            }
        }
        return getInstantiationStrategy().instantiate(beanDefinition, beanName, constructorToUse, args);
    }

    public InstantiationStrategy getInstantiationStrategy() {
        return instantiationStrategy;
    }

    public void setInstantiationStrategy(InstantiationStrategy instantiationStrategy) {
        this.instantiationStrategy = instantiationStrategy;
    }
}
  • 首先在 AbstractAutowireCapableBeanFactory 抽象类中定义了一个创建对象的实例化测量属性类 InstantiationStrategy instantiationStrategy。这里选择 Cglib 的实现类。
  • 接下来抽取 createBeanInstance 方法,在这个方法中需要注意 Constructor 代表了你有多少个构造函数,通过 beanClass.getDeclaredConstructors() 方式可以获取到你所有的构造函数,是一个集合。
  • 最后就需要循环比对出构造函数集合与入参信息 args 的匹配信息。
    • 这里比对方式只是一个数量对比,而 Spring 源码中还需要比对入参类,否则相同数量不同入参类型的情况,就会抛异常。

四、测试:含构造函数的类实例化

4.1 用户Bean对象

UserService.java

package com.lino.springframework.test.bean;

/**
 * @description: 模拟用户 Bean 对象
 */
public class UserService {

    private String name;

    public UserService() {
    }

    public UserService(String name) {
        this.name = name;
    }

    /**
     * 查询用户信息
     */
    public void queryUserInfo() {
        System.out.println("查询用户信息: " + name);
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append(name);
        return sb.toString();
    }
}
  • UserService 中添加一个无参构造函数和有一个 name 入参的构造函数。

4.2 单元测试

4.2.1 单元测试

ApiTest.java

@Test
public void test_BeanFactory() {
    // 1.初始化 BeanFactory
    DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

    // 2.注册bean
    BeanDefinition beanDefinition = new BeanDefinition(UserService.class);
    beanFactory.registerBeanDefinition("userService", beanDefinition);

    // 3.获取bean
    UserService userService = (UserService) beanFactory.getBean("userService", "小零");
    userService.queryUserInfo();
}
  • 次单元测试中,包含三个核心步骤:初始化 BeanFactory 工厂,注册 bean,获取获取 bean
    • 在获取 Bean 对象时候,传递了一个参数名称为 小零 的入参信息,这个信息的传递将会帮我们创建出含有 String 类型构造函数的 UserService 类,而不会再出现初始化报错的问题。

测试结果

查询用户信息: 小零
  • 从测试结果来看,最大的变化就是可以满足带有构造函数的对象,可以被实例化。

4.2.2 无构造函数测试

ApiTest.java

@Test
public void test_newInstance() throws InstantiationException, IllegalAccessException {
    UserService userService = UserService.class.newInstance();
    System.out.println(userService);
}
  • 这个方式的实例化在实现 Spring Bean 容器时直接使用的方式

测试结果

null

4.2.3 有构造函数实例化测试

ApiTest.java

@Test
public void test_constructor() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Class<UserService> userServiceClass = UserService.class;
    Constructor<UserService> declaredConstructor = userServiceClass.getDeclaredConstructor(String.class);
    UserService userService = declaredConstructor.newInstance("小零");
    System.out.println(userService);
}
  • 从最简单的操作来看,如果有构造函数的类需要实例化时,则需要使用 getDeclaredConstructor 获取构造函数,之后通过传递参数进行实例化。

测试结果

小零

4.2.4 获取构造函数信息

ApiTest.java

@Test
public void test_parameterTypes() throws Exception {
    Class<UserService> beanClass = UserService.class;
    Constructor<?>[] declaredConstructors = beanClass.getDeclaredConstructors();
    Constructor<?> constructor = declaredConstructors[1];
    Constructor<UserService> declaredConstructor = beanClass.getDeclaredConstructor(constructor.getParameterTypes());
    UserService userService = declaredConstructor.newInstance("小零");
    System.out.println(userService);
}
  • 这个测试最核心的点在于获取一个类中所有的构造函数,其实也就是这个方法的使用 beanClass.getDeclaredConstructors()

测试结果

小零

4.2.5 Cglib实例化

ApiTest.java

@Test
public void test_cglib() {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(UserService.class);
    enhancer.setCallback(new NoOp() {
        @Override
        public int hashCode() {
            return super.hashCode();
        }
    });
    Object obj = enhancer.create(new Class[]{String.class}, new Object[]{"小零"});
    System.out.println(obj);
}

测试结果

小零

五、总结:含构造函数的类实例化

  • 完善实例化操作,增加 InstantiationStrategy 实例化策略接口,并新增了两个实例化类。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/983511.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

INS惯性导航系统相关技术(概念版)

一、参考资料 组合导航系统生产商 applanix 官网 新手入门系列3——Allan方差分析方法的直观理解 惯性测量单元Allan方差分析详解 IMU标定之—Allan方差 b站视频&#xff1a;武汉大学惯性导航课程合集【2021年秋】 资料下载&#xff1a;PPT&#xff1a;《惯性导航原理与方法》…

nginx-ingress多控制器部署

nginx-ingress直接使用yaml来部署 wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.1/deploy/static/provider/cloud/deploy.yaml修改里面的镜像 多控制器部署实现&#xff1a; 在同一个命名空间中只需要新增一个IngressClass跟 ingress-…

EFG-02-10-31、ERBG-06-B-51电液比例控制调速阀放大器

EFCG-02-10-31、EFG-03-60-26、EFCG-03-125-26、EFCG-03-125-51、EFG-06-250-22、EFCG-06-250-22、EFG-06-250-51、EFCG-02-30-31、ERBG-06-B-51、ERBG-10-C-51、ERBG-06-H-51、ERBG-10-B-51、ERBG-06-C-51、ERBG-10-H-51、EFG-02-10-31、EFCG-03-60-26、EFG-03-125-26、EFG-03…

Mysql存储过程与存储函数

文章目录 1. 简介2. 存储过程的特点3. 存储过程操作语法4. 存储过程变量5. 其它语法6. 存储函数 1. 简介 存储过程是事先经过编译并存储在数据库中的一段SQL集合&#xff0c;调用存储过程可以简化应用开发人员的很多工作&#xff0c;减少数据在数据库和应用服务器之间的传输&a…

生成式AI的JavScript技术栈

如果不使用新的软件基础设施技术&#xff0c;就很难理解它们。 至少&#xff0c;a16z 基础设施团队发现了这一点&#xff0c;而且因为我们中的许多人都是以程序员的身份开始职业生涯的&#xff0c;所以我们经常通过实践来学习。 尤其是生成式AI浪潮的情况尤其如此&#xff0c;它…

使用Blender 在影片剪辑上创建粘滞标签

推荐&#xff1a;使用 NSDT场景编辑器快速搭建3D应用场景 准备场景 步骤 1 在新文件中&#xff0c;右键单击默认立方体和灯光&#xff0c;然后按键盘上的 Del 将其删除。按住 Shift 键&#xff0c;然后右键单击以选择多个对象。 删除默认对象 步骤 2 选择摄像机并按 Alt-R …

问道管理:申购额度如何计算?

在投资领域&#xff0c;很多人会选择申购基金产品。对于大众投资者而言&#xff0c;申购额度是一个较为要害的问题。在此&#xff0c;我们将从多个角度进行剖析&#xff0c;具体解读申购额度怎么核算。 一、基金公司规则 首要&#xff0c;申购额度需要依照基金公司的规则来进行…

js 高精度计算 - decimal.js 库

what decimal.js &#xff1f; decimal.js是一个用于进行精确数值计算的第三方库。通常情况下&#xff0c;当你需要进行对精度要求较高的数值计算时&#xff0c;可以考虑使用decimal.js或类似的库。 JavaScript中的原生Number类型使用双精度浮点数表示&#xff0c;对于某些计算…

torch.cuda.is_available() 解决方案

本人使用的显卡如下&#xff0c;打开任务管理器查看 Anaconda下载哪个版本都可以 使用命令conda create -n pytorch python3.6创建一个名为pytorch的环境&#xff0c;解释器使用3.6的 使用命令conda activate pytorch进入该环境 进入pytorch官网&#xff0c;选择下列选项 …

SPSS教程:手把手教你绘制簇状条形图

SPSS教程&#xff1a;手把手教你绘制簇状条形图 1、问题与数据 某研究者拟分析受教育程度和性别对幸福指数的影响程度&#xff0c;招募了58位研究对象&#xff0c;包括28位男性和30位女性。每一类性别中&#xff0c;研究对象的受教育程度均分为3类&#xff08;高中及以下、大…

Spring原生api操作之如何在spring配置文件添加Bean对象到Spring容器

一、创建一个spring项目 为了使用Spring的原生api&#xff0c;首先要创建一个只基于Spring的项目. 创建maven项目引入spring依赖&#xff08;不要使用6版本以上的&#xff0c;6的最低兼容jdk为jdk17&#xff09;创建spring配置文件 这个配置文件的名称固定&#xff0c;但是必须…

935. 骑士拨号器

935. 骑士拨号器 原题链接&#xff1a;完成情况&#xff1a;解题思路&#xff1a;参考代码&#xff1a; 原题链接&#xff1a; 935. 骑士拨号器 https://leetcode.cn/problems/knight-dialer/description/ 完成情况&#xff1a; 解题思路&#xff1a; 题目要求&#xff1a;键…

网络技术二十:OSPF

OSPF RIP的缺陷 最大跳数限制了网络规模 以跳数为度量值无法准确判断最优路径 路由更新发送完整路由表消耗网络带宽 收敛速度慢 协议会产生路由自环 定义 开放式最短路径优先&#xff0c;基于链路状态特征 工作在IP层&#xff0c;协议号89 OSPF初始化流程 1.建立邻居和邻…

实现无公网IP的公网环境下Windows远程桌面Ubuntu 18.04连接,高效远程办公!

文章目录 一、 同个局域网内远程桌面Ubuntu1. 更新软件仓库2. 安装支持包3. 安装XFCE4桌面环境4. 安装XRDP5. 环境设置5.1 XFCE桌面配置5.2 在配置文件中&#xff0c;加入XFCE会话 6 重启服务7. 查看IP地址8. 使用Windows远程桌面连接 二、公网环境系统远程桌面Ubuntu1. 注册cp…

解锁前端Vue3宝藏级资料 第一章 带你深入了解Vue3项目创建 1 (Vue CLI 创建vue项目)

Vue.js是一款广受欢迎的JavaScript框架&#xff0c;专为创建网站、web应用程序和管理系统等前端用户界面(UI)设计。其流行不仅因为它拥有庞大的开发者社区和丰富的学习资源&#xff0c;还因为它具有低学习成本和易于上手的特点。当你在使用中遇到疑问或困难时&#xff0c;可以轻…

漏洞修复-SSH版本信息可被获取漏洞

漏洞修复-SSH版本信息可被获取漏洞 1、背景2、环境3、思路4、实操 1、背景 新分配下来的云服务&#xff0c;在没有投入生产环境之前&#xff0c;漏扫和安全防固是两项基本工作。云安全产品扫描过后导出的漏洞信息如下&#xff1a; 漏洞详情&#xff1a; 威胁分值 &#xff1…

pg分组过滤

方法&#xff1a; 注意&#xff1a; 出现多个字段&#xff0c;必须要在group by中添加分组依据。或者聚合函数不用 where过滤分组前的&#xff0c;having过滤分组后的&#xff0c;count是分组后产生的

外汇MT4实战技巧:利用挂单和追踪止损提升交易效果

外汇交易是一项高风险的投资活动&#xff0c;需要交易者具备良好的市场分析能力和实战经验。对于MT4&#xff08;可在mtw.so/6gwPno这点下&#xff09;交易平台的使用者来说&#xff0c;掌握挂单和追踪止损这两种实战技巧可以有效提升交易效果。本文将为您详细介绍这两种技巧的…

Ivanti Sentry 身份验证绕过漏洞 CVE-2023-38035

Ivanti Sentry 身份验证绕过漏洞 CVE-2023-38035 最近&#xff0c;网络上披露了Ivanti Sentry&#xff08;以前称为MobileIron Sentry&#xff09;中发现的漏洞。此漏洞影响版本为 9.18 及更早版本。此漏洞不会影响其他 Ivanti 产品&#xff0c;例如 Ivanti EPMM 或 Ivanti Ne…

LabelImg标注快捷键

LabelImg标注快捷键 由于吃了屎的原因&#xff0c;在标注数据 其实快捷键都有显示 保存标注文件&#xff1a;ctrls A&#xff1a;切换到上一张图片 D&#xff1a;切换到下一张图片 W&#xff1a;调出标注十字架 del &#xff1a;删除标注框框 Ctrlu&#xff1a;选择标注的图…