2. El punto de partida del código fuente de Spring: BeanDefinition

★¿Por qué empezar con BeanDefinition?

El código fuente de Spring es demasiado grande. Realmente no es fácil dominar el código fuente de Spring, y es aún más difícil entenderlo. Aprender el código fuente de Spring es un gran proyecto. Muchos lectores analizan el código fuente de depuración línea por línea desde el inicio del contenedor de primavera. Es fácil de entender al principio, pero cuando profundice en el código fuente de depuración capa por capa, sentirá "Estoy en esta montaña, No conozco la cara real".

El autor también experimentó este tipo de dolor al principio. Después de varias lecturas intensivas, siento que el código fuente de Spring no se puede leer desde el principio. Necesitamos comprender los componentes básicos de Spring y la relación entre los componentes. Esto es como ensamblar una computadora. Primero debe comprender la función de la CPU, la memoria, la placa base y el disco duro, y luego saber cómo ensamblarlos. Leer el código fuente desde el principio es como pedirle a un estudiante de finanzas que ensamble un computadora El efecto se puede imaginar Y saber.

imagen.png

Si te pregunto el papel de la primavera? Presumiblemente, la mayoría de los programadores responderán "objetos de gestión, desarrollo conveniente".

Sí, todo en Java es un objeto. Spring Encapsulation gestiona los objetos que creas. Los objetos encapsulados se denominan beans. Los beans amplían tus objetos. Todas las propiedades y métodos de los objetos que creas son un subconjunto del bean.

Todas las funciones de resorte giran en torno a este frijol. El nombre específico de este frijol en primavera es BeanDefinition.

★Comprender el papel de BeanDefinition

Si escribe un archivo Person.java y lo compila en un archivo Person.class, jvm cargará el archivo de clase en la memoria de la máquina virtual (área de método), y el programa generará objetos en el montón de acuerdo con la clase cuando encuentre la nueva palabra clave.

El archivo de clase se ha generado y Spring no puede cambiar su clase, pero ¿qué sucede si desea ampliar su clase? Muy simple, Spring define una clase BeanDefinition para encapsular aún más la clase Person, Person es solo un atributo en BeanDefinition y finalmente se registra en el contenedor Spring.

BeanDefinition puede entenderse como el archivo .java en Java. Cuando el archivo .java no está compilado en un archivo de clase, si podemos hacer modificaciones arbitrarias al archivo .java, entonces se puede lograr el propósito de la expansión.

Una vez completada nuestra expansión, compílela en un archivo .class y ejecútela.

Lo mismo ocurre con BeanDefinition, podemos modificar los métodos y propiedades de BeanDefinition que afectan a las clases a través de postprocesadores. Finalmente, Spring instancia la BeanDefinition modificada o extendida para lograr el propósito de la extensión.

imagen.png

BeanDefinition también encapsula otros atributos relacionados con Person, como el alcance del bean, el modelo de inyección del bean, si el bean tiene carga diferida, etc.

Si compara Person.java con PersonBeaDefiniton:

imagen.png

De la misma manera, si no entiende BeanDefinition, no puede leer el código fuente de Spring. La beanDefintion es aburrida y oscura, pero muy, muy importante. Si desea leer el código fuente de Spring detenidamente, lea el conocimiento de beanDefintion tres veces detenidamente. ¡Es la piedra angular del marco Spring y no puede evitarlo!

★El proceso de carga de BeanDefinition

clase de configuración

package tyrant;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
//包扫描
@ComponentScan("tyrant")
//使用xml文件启动
@ImportResource("applicationContext.xml")
public class Config {
}

archivo de configuración xml

    <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
             >
          <!--注意上面的default-autowire="byType"  根据type进行自动注入-->
          <bean id="ab" abstract="true" ></bean>
          <bean id="xxx" class="tyrant.InterService" parent="ab"></bean>
      </beans>

La forma xml de comenzar no es muy práctica. La función más importante de xml es la inyección de dependencia (hablaremos de esto más adelante, pero puede ignorarse). En la explicación de seguimiento, el autor básicamente lo explica a modo de anotación. .

clase de negocios

    package tyrant;
    @Component
    @Description("业务类")
    @Scope("singleton")
    public class InterService {
        private String name;
        private int age;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }

clase de prueba

    package tyrant;
    import org.springframework.beans.factory.config.BeanDefinition;
    import org.springframework.beans.factory.support.GenericBeanDefinition;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;

    public class SpringTest {
        public static void main(String[] args) throws InterruptedException {
            start1();
        }
        
         public static void start1() {
            //其中Config.class可以指定为
            //包扫描启动:@ComponentScan("tyrant")
            //使用xml文件启动:@ImportResource("applicationContext.xml")
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
            //注册配置类
            context.register(Config.class);
            //加载或者刷新当前的配置信息
            context.refresh();
        }
        public static void start2() {
            //其中Config.class可以指定为
            //包扫描启动:@ComponentScan("tyrant")
            //使用xml文件启动:@ImportResource("applicationContext.xml") 
            AnnotationConfigApplicationContext context = new            
                                                    AnnotationConfigApplicationContext(Config.class);
            //加载或者刷新当前的配置信息
            context.refresh();
        }
    }

El inicio de Spring hará mucho trabajo, y es imposible explicar todo el proceso de inicio claramente en el primer artículo del tema, para conducir a BeanDefinition.

El autor explica tres procesos:

  • El escaneo Spring primero escanea las clases que cumplen con las reglas del paquete com de acuerdo con la anotación de configuración @ComponentScan("com"). Aquí solo existe la clase InterService;
  • Análisis Analizar la clase InterService es principalmente para obtener todas las anotaciones: @Component, @Description("clase empresarial"), @Scope("singleton")
  • 封装 解析完的信息保存到哪里里去呢? 就存在BeanDefinition中。BeanDefinition就是对你的InterService进行建模。

BeanDefinition的源码

其实,BeanDefinition只是一个接口:

public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
     //InterService的scope值,单例
    String SCOPE_SINGLETON = "singleton";
    //InterService的scope值,非单例
    String SCOPE_PROTOTYPE = "prototype";
    //Bean角色:
    //用户
    int ROLE_APPLICATION = 0;
    //某些复杂的配置
    int ROLE_SUPPORT = 1;
    //完全内部使用
    int ROLE_INFRASTRUCTURE = 2;
    //返回此InterService的的父bean定义的名称
    //如果有的话 <bean parent="">(xml配置文件中的bean配置)
    String getParentName();    
    void setParentName(String var1);
    //设置或获取业务类全路径名
    void setBeanClassName(String var1);
    String getBeanClassName();
    //InterService的scope值
    void setScope(String var1);
    String getScope();
   //InterService的懒加载设置及获取
    void setLazyInit(boolean var1);
    boolean isLazyInit();
    //InterService的依赖对象 ,表示InterService实例创建前应该先创建哪个对象
    void setDependsOn(String... var1);
    String[] getDependsOn();
    //是否为被自动装配
    void setAutowireCandidate(boolean var1);
    boolean isAutowireCandidate();
   //如果是被自动装配,是否为主候选bean
    void setPrimary(boolean var1);
    boolean isPrimary();
   //定义创建该Bean对象的工厂类 
    void setFactoryBeanName(String var1);
    String getFactoryBeanName();
   //定义创建该Bean对象的工厂方法 
    void setFactoryMethodName(String var1);
    String getFactoryMethodName();
   //返回此InterService的构造函数参数值。
    ConstructorArgumentValues getConstructorArgumentValues();
   //获取普通属性集合
    MutablePropertyValues getPropertyValues();
   //是否为单例
    boolean isSingleton();
    //是否为原型
    boolean isPrototype();
    //是否为抽象类
    boolean isAbstract();
    //获取这个bean的应用
    int getRole();
   //返回对bean定义的可读描述。
    String getDescription();
   //返回该bean定义来自的资源的描述(用于在出现错误时显示上下文)
    String getResourceDescription();
    BeanDefinition getOriginatingBeanDefinition();
}

通过BeanDefinition接口可以对InterService的附加属性进行设置和获取,虽然目前不能完全搞懂所有属性及方法,建议把这个接口代码都看一遍。

在这里建议大家读源码一定要“慢下来”,一点一点的啃,切忌浮躁,这个痛苦的过程过去了你会有种高屋建瓴的感觉! 光读不行,还得写代码调试从而增加印象,比如上面BeanDefinition接口中的方法我想验证一下,很简单,上代码:


package tyrant;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringTest {
    public static void main(String[] args) throws InterruptedException {
    
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        //注册配置类
        context.register(Config.class);
    
        //加载或者刷新当前的配置信息
        context.refresh();;
        
        //获取InterService对应的BeanDefinition 
        //默认名称为interService,关于名字的更改以后讲。
        BeanDefinition interServiceBeanDefinition = context.getBeanDefinition("interService");

        System.out.println("——————InterService的附加属性如下:");
        System.out.println("父类"+interServiceBeanDefinition.getParentName());
        System.out.println("描述"+interServiceBeanDefinition.getDescription());
        System.out.println("InterService在spring的名       
                           称"+interServiceBeanDefinition.getBeanClassName());
        System.out.println("实例范围"+interServiceBeanDefinition.getScope());
        System.out.println("是否是懒加载"+interServiceBeanDefinition.isLazyInit());
        System.out.println("是否是抽象类"+interServiceBeanDefinition.isAbstract());
        System.out.println("——————等等等等,读者自行编写");
    }
}

imagen.png

注意,这些属性在xml配置文件中都能找到对应匹配的标签,读者可用xml配置文件自行进行配置,只需要将Config配置类上的@ImportResource("applicationContext.xml")注解打开即可。

既然BeanDefinition是个接口,那spring中肯定有他的实现类对不对,好,是时候看一下BeanDefinition的类继承图了。

在这里笔者跟大家说一个问题,笔者发现很多人读源码的时候拿来一个类就读或者debug源码跟读,也不管这个类跟其他类的关系,读完后感觉很混乱,甚至吐槽spring源码写的毫无章法,拜托,spring源码是典型的面向接口编程,严格遵循开闭原则、依赖倒置原则和迪米特法则,是spring源码写的差还是你的水平差?

spring每一个模块都有一个完整的类继承关系图,不然spring被业界称赞的高扩展性谈何而来?所以我们必须将每个模块的类继承关系了然于胸。

初学,我们也不可能将继承体系中的每个类都搞懂,把这个继承图下载下来存到桌面,在以后的源码阅读中这个继承关系会被你一一攻破,学完你也就掌握了,而且不会忘,更能提高你的编程水平,读完spring你会发现的编程风格潜移默化的被spring影响了!对吧。

BeanDefinition的继承关系

imagen.png

硬编码注入BeanDefinition

以前利用spring开发大都通过xml方式进行bean配置,其实spring框架开发之初并不想让程序员通过xml方式进行配置,而是通过代码让我们手动将InterService封装成对应的BeanDefinition,看下面的代码:

    //首先,删掉业务类InterService所有的注解,不让他被扫描到,spring根据注解判断是否将类进行包装。
    package tyrant;
    public class InterService {
        private String name;
        private int age;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }

 

    public class SpringTest {
        public static void main(String[] args) throws InterruptedException {
            AnnotationConfigApplicationContext context = new 
                                    AnnotationConfigApplicationContext();
            //注册配置类
            context.register(Config.class);
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClassName("com.InterService");
            beanDefinition.setScope("singleton");
            beanDefinition.setDescription("手动注入");
            beanDefinition.setAbstract(false);
            //将beanDefinition注册到spring容器中
            context.registerBeanDefinition("interService",beanDefinition);
            //加载或者刷新当前的配置信息
            context.refresh();
            
            BeanDefinition interServiceBeanDefinition = context.getBeanDefinition("interService");
            System.out.println("——————InterService的附加属性如下:");
            System.out.println("父类"+interServiceBeanDefinition.getParentName());
            System.out.println("描述"+interServiceBeanDefinition.getDescription());
            System.out.println("InterService在ioc的名称"+interServiceBeanDefinition.getBeanClassName());
            System.out.println("实例范围"+interServiceBeanDefinition.getScope());
            System.out.println("是否是懒加载"+interServiceBeanDefinition.isLazyInit());
            System.out.println("是否是抽象类"+interServiceBeanDefinition.isAbstract());
            System.out.println("——————等等等等,读者自行编写");
            
        }
    }

输出:

——————InterService的附加属性如下:
父类null
描述手动注入
InterService在ioc的名称tyrant.InterService
实例范围singleton
是否是懒加载false
是否是抽象类false
——————等等等等,读者自行编写

其实笔者很喜欢这种代码方式完成spring配置工作,这样能让我们更深入的了解和应用spring,不过这种方式的缺点也很明显-繁琐易出错,spring为了简化我们的工作提供了xml配置方式,直到spring5.x注解方式的稳定成熟,spring全家桶得到了飞速的发展。但通过这个例子读者可以加深对BeanDefinition的理解。

★IOC的引出

看上面这么一行代码:

 context.registerBeanDefinition("interService",beanDefinition);

这行代码的意思是将我们手动封装的beanDefinition注册到容器中,同时给这个beanDefinition起了个名字"interService",spring内部生成beanDefinitino时会默认起一个名字,改名字的规则就是业务类名字首字母小写。 那生成的BeanDefinition保存在哪里呢?既然我们是通过上面的方法将BeanDefinition注册到容器中,肯定是在这个方法底层实现了保存,我们点进去看。

     @Override
        public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
                throws BeanDefinitionStoreException {
            // 将beanDefinition保存到spring容器中
            this.beanFactory.registerBeanDefinition(beanName, beanDefinition);
        }

继续跟进registerBeanDefinition方法,找到下面这行代码:

  this.beanDefinitionMap.put(beanName, beanDefinition);

顾名思义,beanDefinitionMap就是一个Map呀!

具体是啥map,我们ctrl+鼠标左键单击找到beanDefinitionMap定义处:

/* Map of bean definition objects, keyed by bean name. */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);

总之,这就是beanDefinition存储的容器,这行代码所在的类名叫DefaultListableBeanFactory。

它是bean的工厂,spring中所有的对象或者说bean都存在这个bean工厂中,业界叫它IOC,很多书或者视频都会讲IOC,相信读者也知道IOC是容器,但它就是一堆Map集合而已,beanDefinitionMap只是众多Map中的一个而已,以后我会将其他的map容器,今天你只需要只到这么一个存放BeanDefinition的容器即可。

IOC是什么?

IOC的全称是Inversion of Control,翻译过来就是控制反转。控制反转的意思就是我们将对象的创建和管理权交给IoC Service Provider(IoC思想的具体实现)

控制反转IOC是一种设计思想,DI依赖注入是实现IoC的一种方法。

IOC是Spring框架的核心内容,它使用多种方式完美的实现了Ioc,可以使用XML配置,也可以使用注解,新版本的Spring也可以使用完全的Java类加注解实现零配置的IoC,但这种实现方式再SpringBoot中比较常见。

为什么要使用IOC

为了降低**耦合度。

个人理解:接口HumanService有多个实现类UserServiceImpl、PeopleServiceImpl、HumanServiceImpl。 一开始我们用的是UserServiceImpl,可以这么写

HumanService humanService = new UserServiceImpl();

当我们业务发生变化,需要使用PeopleServiceImpl,此时需要手动修改代码为:

HumanService humanService = new PeopleServiceImpl();

假如我们有100个地方都需要这个用到HumanService,那么我们需要硬编码修改100处代码。

假如有一天我们要改为HumanServiceImpl,又要硬编码修改100处代码。

痛,真的太痛了!

但是如果使用ioc,那我们可以这么写

    @Autowired
    private HumanService humanservice;

当我们需要UserServiceImpl,我们只需要注入UserServiceImpl。

    @Service
    public class  UserServiceImpl implements HumanService{
    }

此时ioc容器会帮我们自动注入HumanService对应的实现为UserServiceImpl。 当我们业务发生变化,需要使用PeopleServiceImpl。只需要把UserserviceImpl对应的实现类上面的@Service去掉加到PeopleServiceImpl上即可实现替换。相比较而言使用ioc依赖注入比硬编码new出来对象要优雅很多!

★DefaultListableBeanFactory创建时机

有读者问,那DefaultListableBeanFactory这个bean工厂啥时候创建的?看代码:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

这是我们创建spring上下文对象,AnnotationConfigApplicationContext 类有一个父类,AnnotationConfigApplicationContext的无参构造函数执行时会默认调用父类无参构造函数,AnnotationConfigApplicationContext的父类如下:

    public class AnnotationConfigApplicationContext 
                        extends GenericApplicationContext 
                                        implements AnnotationConfigRegistry 

也就是GenericApplicationContext ,我们看一下他的构造函数:

    public GenericApplicationContext() {
        //初始化一个BeanFactory
        this.beanFactory = new DefaultListableBeanFactory();
    }

也就是说,spring启动的时候就创建好了这个bean工厂!

咦?不是要讲BeanDefinitino的继承关系吗?怎么跑偏了?

这就是spring的特点,太庞大了,没有孤立的知识点!这也是很多读者阅读spring源码时读着读着就蒙圈的原因。

★后置处理器的引出

上文我们通过手动将InterService封装成了一个BeanDefinition然后注册(put到了beanDefinitionMap中)到了容器中,我说了现在我们没有这么用的了,都是spring自动帮我们完成扫描注册,在哪完成的扫描注册?回到下面这几行代码:

     AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    //注册配置类
    context.register(Config.class);
    //加载或者刷新当前的配置信息
    context.refresh();

context.refresh()方法,完成了spring的启动、扫描、解析、实例化等一系列过程,这个方法完成的功能太多了,我们的扫描注册也是在这里完成的,进入到这个方法,找到这么一行代码:

invokeBeanFactoryPostProcessors(beanFactory);

翻译一下名字,执行bean工厂的后置处理器,这行代码完成了扫描与注册,我不带大家分析里面的代码,你只需要知道他的作用就行,这行代码执行完成后,我们只是把业务类InterService封装成了BeanDefinition而已,业务类InterService并没有实例化,在业务类InterService实例化之前我们能不能从beanDefinition中将InterService偷梁换柱呢?

或者说,我们能通过BeanDefinition来构建bean,那我们能不能修改bean呢?那必须的! 通过后置处理器完成,什么是后置处理器?可以把它理解成回调,我扫描注册成功后回调后置处理器!BeanDefinition讲完后紧接着就讲后置处理器。我们添加一个后置处理器:

    /**
     * 扫描注册成功完成后
     * spring自动调用后置处理器MyBeanFactoryPostProcessor的postProcessBeanFactory方法
     */
    @Component
    public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
                throws BeansException {
            //通过bean工厂拿到业务类InterService的beanDefinition
            GenericBeanDefinition beanDefinition =
                    (GenericBeanDefinition) beanFactory.getBeanDefinition("interService");
            System.out.println("扫描注册成功完成后,spring自动调用次方法");
            System.out.println(beanDefinition.getDescription());
        }
    }

spring扫描注册完成后,会自动调用MyBeanFactoryPostProcessor的postProcessBeanFactory方法,这个方法给你传递了一个ConfigurableListableBeanFactory类型的bean工厂。

ConfigurableListableBeanFactory是一个接口,上文spring启动实例化的DefaultListableBeanFactory工厂是它的实现类。天啊,竟然把bean工厂给你了,相当于敌人把军火库暴露在你面前,你岂不是想干嘛就干嘛!

上述代码我们通过bean工厂拿到了业务类InterService的beanDefinition,我都拿到你的beanDefinition了,那么我不但可以get到你的信息,我也可以set你的信息从而改变你的行为来影响你后续的实例化。我们来编写另一个业务类:

    package tyrant;
   
    public class User {
        private int age =31;
        private String name="user";
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }

spring启动时也会把这个业务类扫描,接下来,看好了。

我在bean工厂中偷梁换柱,在beanDefinition中将InterService业务类替换掉:

        package tyrant;
        
        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.GenericBeanDefinition;
        import org.springframework.stereotype.Component;
        
        /**
         * 扫描注册成功后,spring自动调用MyBeanFactoryPostProcessor的postProcessBeanFactory方法
         */
        @Component
        public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
            @Override
            public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
                    throws BeansException {
                BeanDefinition interService = beanFactory.getBeanDefinition("interService");
                
                
                /***
                getBeanDefinition方法返回的BeanDefinition类型为什么强转成GenericBeanDefinition?
                因为BeanDefinition接口中并没有setBeanClass这个方法
                GenericBeanDefinition是他的实现,提供更丰富的功能。
                不同的BeanDefinition实现具有不同的作用。
                ***/ 
                if(interService instanceof GenericBeanDefinition){
                    GenericBeanDefinition beanDefinition =
                            (GenericBeanDefinition) interService;
                    System.out.println("修改后的名字:" + beanDefinition.getBeanClassName());
                    //注意这里修改了class类型
                    beanDefinition.setBeanClass(User.class);
                }
            }
        }
     

        public class Test {
            public static void main(String[] args) {
                AnnotationConfigApplicationContext context 
                                    = new AnnotationConfigApplicationContext();
                //注册配置类
                context.register(Config.class);
                context.refresh();
                System.out.println("更改后的业务类:"+
                                 context.getBeanDefinition("interService").getBeanClassName());
            }
        }

打印结果:

修改的名字:tyrant.InterService
更改后的业务类:tyrant.User

Ahora que hemos reemplazado el InterService robando el haz, Spring no instanciará el InterService en el proceso de creación de instancias posterior.

Mira el código a continuación:

public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new 
                                            AnnotationConfigApplicationContext();
        //注册配置类
        context.register(Config.class);
        context.refresh();
        //更改后的业务类:tyrant.User
        System.out.println("更改后的业务类:"+
                            context.getBeanDefinition("interService").
                                                            getBeanClassName());
        //尝试获取名为interService的BeanDefinition会报错
        //因为在MyBeanFactoryPostProcessor中执行了以下代码     
        //beanDefinition.setBeanClass(User.class);
        context.getBean(InterService.class);
    }
}

Se informa un error en el resultado de la impresión, porque no hay BeanDefinition llamado interService en Spring.

Resumir

Este artículo describe principalmente la función de BeanDefinition y el proceso de carga, lo que lleva al contenedor IOC y la función del contenedor IOC. Finalmente, se introduce el post-procesador.A través del post-procesador, podemos transformar aún más el BeanDefinition.

Supongo que te gusta

Origin juejin.im/post/7234887157000175674
Recomendado
Clasificación