Montagem do Spring Bean em profundidade

Tian Ji

Os cavalos são divididos em três categorias: superior médio, inferior e superior.

Os beans dependentes do ambiente aparecerão nos aplicativos.Em termos de cenários de uso da fonte de dados, os bancos de dados incorporados podem ser usados ​​ao desenvolver funções simples,
enquanto os ambientes de produção geralmente usam bancos de dados com participação de mercado. Não mudaremos para
o bean de fonte de dados de um ambiente específico ao publicar no ambiente de produção de teste e, em seguida, recompilar e liberar.

Anotação @Profile

O Spring inicialmente forneceu anotações de perfil para tratar de beans específicos no ambiente de montagem.

@Configuration
public class HxDataSourceConfig {
    @Bean
    @Profile("dev")
    public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScripts(new String[]{"hxschema.sql",
                        "hxdata.sql"})
                .build();
    }

    @Bean
    @Profile("prod")
    public DataSource mysqlDataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("root");
        datasource.setPassword("h123");
        return datasource;
    }
}

Verificar

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HxDataSourceConfig.class)
//激活dev配置
@ActiveProfiles("dev")
public class HxDataSourceConfigTest {

    @Autowired
    private DataSource dataSource;

    //当前激活dev配置,embeddedDataSource bean被创建
    @Test
    public void testEmbeddedDataSourceBean() {
        assert (dataSource instanceof EmbeddedDatabase);
    }
}

Além disso, o @Profile também pode ser anotado no nível da classe

@Configuration
@Profile("dev")
public class HxDataSourceConfig {
}

Hands-on

Se a anotação @ActiveProfiles não for adicionada à classe HxDataSourceConfigTest ou @ActiveProfiles não especificará um valor de anotação.
O que vai acontecer?

Nenhum dos dois beans no exemplo acima retornará. O programa lança NoSuchBeanDefinitionException

Se o dev estiver ativado na classe HxDataSourceConfigTest, @ActiveProfiles ("dev"). Mas o mysqlDataSource
não adiciona nenhum comentário, então o bean mysqlDataSource será criado? Modifique a classe de configuração para definir apenas um
bean mysqlDataSource e não use a anotação @Profile.

Os grãos sem uma anotação de perfil não estão sujeitos ao perfil atualmente ativado e ainda serão criados. Portanto, o perfil ativado
restringirá apenas os beans que usam a declaração de perfil para depender de um ambiente específico

@Configuration
public class HxDataSourceConfig {
    @Bean
    public DataSource mysqlDataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("root");
        datasource.setPassword("h123");
        return datasource;
    }
}

//test
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HxDataSourceConfig.class)
@ActiveProfiles("dev")
public class HxDataSourceConfigTest {

    @Autowired
    private DataSource dataSource;

    //测试通过
    @Test
    public void testmysqlDataSource() {
        Assert.notNull(dataSource);
    }
}

Configuração de perfil baseada em assembly XML

<!--方式一,基于环境创建多个定义bean的xml-->
<!--dev.xml-->
<beans profile="dev">
    <bean></bean>
</beans>

<!--prod.xml-->
<beans profile="prod">
    <bean></bean>
</beans>

<!--方式二,所有环境bean都放在一个xml,用beans嵌套-->
<beans>
    <beans profile="dev">
        <bean></bean>
    </beans>

    <beans profile="prod">
        <bean></bean>
    </beans>
</beans>

ativação de perfil

No teste de unidade acima, já desejamos ativar a solução de bean configurada por perfil durante o teste.
Use a anotação @ActiveProfiles na classe

@ActiveProfiles("dev")
public class HxDataSourceConfigTest{
}

O Spring fornece spring.profiles.active e spring.profiles.default para ativar mais de um perfil

Se spring.profiles.active estiver configurado, aceite seu valor; caso contrário, spring.profiles.default
esteja configurado, aceite -o; caso contrário, nenhum perfil estará ativo e quaisquer beans que declarem perfis não serão criados.

Que outras maneiras podem ser definidas spring.profiles.active e spring.profiles.default?

Por exemplo, configure o valor spring.profiles.default em web.xml durante o desenvolvimento

<!--web.xml-->
<web-app>
    <!--other omitted for simplification-->
    <context-param>
        <param-name>spring.profiles.default</param-name>
        <param-value>dev</param-value>
    </context-param>
    <servlet>
        <servlet-name>delegateServlet</servlet-name>
        <servlet-class>designpattern.delegatepattern.mock.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>spring.profiles.default</param-name>
            <param-value>dev</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
</web-app>

Configure o valor spring.profiles.active por meio de variáveis ​​de ambiente, propriedades do sistema JVM ou entradas JNDI durante a implementação

@ Anotação tradicional

A anotação @Profile é específica para definir condições ambientais. Mas a criação do bean pode ter que considerar outras dependências. No momento, a
anotação @Profile é impotente e precisa de uma solução mais geral de anotação condicional: @Conditional está chegando

Como usar

Leia primeiro o javadoc anotado por @Conditional, isso é muito importante, lendo o documento para entender

  1. O componente especificado por valor será registrado somente depois que as condições definidas por todas as classes que implementam a interface Condição especificada por valor forem atendidas.
  2. condição é um estado que pode ser determinado programaticamente. O resultado determinado é realmente um tipo booleano
  3. O @Conditional pode ser usado como uma anotação em nível de tipo diretamente em qualquer classe ou indiretamente, junto com @Component, @Configuration como
    uma anotação personalizada de combinação de meta-anotação.
  4. A anotação @Conditional pode ser usada como uma anotação no nível do método nos métodos do bean.
  5. Quando @Conditional é usado na classe @Configuration, os métodos de bean na classe, @ComponentScan e
    @Import relacionados à classe estão sujeitos a condições.
  6. A condição anotada por @Conditional não é herdada. A condição da superclasse será ignorada pela subclasse.

Em segundo lugar, devemos fazer perguntas para praticar a verificação e compreensão delas.

//让我们继续修改HxDataSourceConfig类如下
@Configuration
public class HxDataSourceConfig {
    @Bean
    //这边使用@Conditional注解
    @Conditional(JdbcPropertyExist.class)
    public DataSource mysqlDataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("root");
        datasource.setPassword("h123");
        return datasource;
    }
}

//设定条件
//application.properties文件存在hxdatasource.jdbcDriver属性则返回true
class JdbcPropertyExist implements Condition {
    //本示例定义的条件并没有依赖ConditionContext及AnnotatedTypeMetadata接口参数
    //但实际使用时,可能需要详细了解这两个接口提供的特性
    @Override
    public boolean matches(ConditionContext context,
                           AnnotatedTypeMetadata metadata) {
        InputStream inputStream = JdbcPropertyExist.class.getClassLoader()
                .getResourceAsStream("application.properties");
        Properties properties = new Properties();
        try {
            properties.load(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return properties.stringPropertyNames()
                .contains("hxdatasource.jdbcDriver");
    }
}

## application.properties文件
## 文件里的属性决定了mysqlDataSource是否会创建
hxdatasource.jdbcDriver

Verificar

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HxDataSourceConfig.class)
public class HxDataSourceConfigTest {
    @Autowired
    private DataSource dataSource;

    @Test
    public void testmysqlDataSource() {
        Assert.notNull(dataSource);
    }
}

Ambiguidade da montagem automática

"Viagem ao oeste - o verdadeiro e falso rei dos macacos"

Guanyin: Qual de vocês é Goku?

Os feijões no Spring são montados automaticamente, mas se houver vários beans que atendam às condições, o Spring estará com problemas.
Vamos adicionar algum material a ele.

@Configuration
public class HxDataSourceConfig {
    @Bean
    public EmbeddedDatabase embeddedDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScripts(new String[]{"hxschema.sql",
                        "hxdata.sql"})
                .build();
    }

    @Bean
    public DriverManagerDataSource mysqlDataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("root");
        datasource.setPassword("h123");
        return datasource;
    }
}

Verificar

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HxDataSourceConfig.class)
public class HxDataSourceConfigTest {
    @Autowired
    private DataSource dataSource;

    @Test
    public void testmysqlDataSource() {
        Assert.notNull(dataSource);
    }
}

Spring: Não foi possível autowire o campo NoUniqueBeanDefinitionException

Declaramos que o campo injetado automaticamente é o tipo de interface DataSource e
os dois tipos de beans que definimos na classe de configuração HxDataSourceConfig implementam a interface DataSource. Portanto, a Primavera não pode fazer
escolhas arbitrárias sem ajuda .

Mas a balabala continuará assim mesmo, podemos usar o @Primary para anotar um dos beans. Deixe-o de
pé e compartilhe quando o Spring estiver com problemas . Isso selecionou o Rei Macaco para a Montanha Huaguo.

@Bean
@Primary
public EmbeddedDatabase embeddedDataSource() {
}
<!--xml中配置primary bean-->
<bean primary="true"></bean>

Naquele dia, outro belo rei dos macacos também clamava por um sutra.

Quando existem vários @Primary, a primavera é novamente confusa. NoUniqueBeanDefinitionException, mais de um bean 'primário' encontrado entre os candidatos
.

Mas a balabala continua assim mesmo, a primavera fornece uma solução qualificadora: @Qualifier. Pode-se entender que os
requisitos para ir à Bíblia são mais rigorosos, de 'rei dos macacos da beleza' a 'rei das belezas dos macacos de pedra'. O macaco de pedra dentro é na verdade uma limitação. Voltar ao exemplo

//在自动注入点使用@Qualifier
@Autowired
@Qualifier("mysqlDataSource")
private DataSource dataSource;

O valor em @Qualifier no ponto de injeção pode corresponder a 2 cenários

  1. Use @Qualifier para declarar um bean com valor
  2. Nenhum bean com valor de ID @Qualifier

Portanto, o ponto de injeção acima pode corresponder à seguinte situação

//场景一 bean不使用@Qualifier
//方法名和注入点@Qualifier的value相同的bean方法
//或@Component注解的类名和value相同的类(除了首字母大小写)
@Bean
public DriverManagerDataSource mysqlDataSource() {
}

//场景二:bean显式使用@Qualifier声明
@Bean
@Qualifier("mysqlDataSource")
public DriverManagerDataSource mysqlDataSource() {
}

 O valor de @Qualifier usando o ID do bean padrão como ponto de injeção parece ser simples e direto.
 Mas dessa forma, o código do ponto de injeção e o método do bean injetado ou o nome da classe são fortemente acoplados. Quando o método do bean ou o
nome da classe é refatorado, o código do ponto de injeção precisa ser modificado. Considerando isso, você pode usar o método bean ou @Component
Use @Qulifier na classe para personalizar um qualificador com um significado claro e, em seguida, use esse qualificador no ponto de injeção.
 Ou se você usar o IDEA para desenvolver, não há problema em usar o ID do bean padrão, porque ao refatorar o método do bean no IDEA, o ponto de injeção pode ser detectado e refatorado de forma inteligente.

Anotação personalizada @Qualifier

Até agora, tudo está funcionando bem, vamos criar um pequeno problema.

 Continuando a história anterior do belo rei dos macacos, agora assumimos essa cena, o rei dos macacos de pedra não é o único, e há um no oeste.
 Em seguida, continuamos usando o @Qualifier para abreviar o intervalo de correspondência. Por exemplo, no ponto de definição do bean, o ponto de injeção é adicionado com @Qualifier ("石猴 美 猴王") e @Qualifier ("西西")

O problema é que a anotação @Qualifier do Spring não suporta vários usos na mesma entrada

A solução é usar uma anotação qualificadora personalizada, o método é muito simples, a anotação interna pode usar a anotação @Qualifier

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Dev {
}

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Test {
}

@Configuration
public class HxDataSourceConfig {

    @Bean
    @Qualifier("mysql")
    @Dev
    public DriverManagerDataSource mysqlDataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("dev");
        datasource.setPassword("h123");
        return datasource;
    }

    @Bean
    @Qualifier("mysql")
    @Test
    public DriverManagerDataSource mysqlDataSource2() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("test");
        datasource.setPassword("h123");
        return datasource;
    }
}

Verificar

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HxDataSourceConfig.class)
public class HxDataSourceConfigTest {
    @Autowired
    @Qualifier("mysql")
    @Dev
    private DataSource dataSource;

    @Test
    public void testmysqlDataSource() {
        //这边输出dev下的用户名,因为上面使用了@Dev
        System.out.println(((DriverManagerDataSource)dataSource).getUsername());
        Assert.notNull(dataSource);
    }
}

Ainda existe um problema aqui: é difícil imaginar que o uso do @Qulifier não possa resolver o problema da ambiguidade do bean, mas precise usar várias
anotações de qualificador personalizado .

Um motivo que pode ser adivinhado é que ele não foi bem projetado no início e o código subseqüente não é fácil de alterar o valor do qualificador à vontade.Você
só pode adicionar um bean que é refatorado como uma anotação personalizada

Mas as vantagens dessa anotação de qualificador personalizado ainda são muito óbvias

  1. Os nomes das anotações são qualificadores auto-explicativos; você não precisa atribuir um valor como @Qualifier, como nosso @Dev acima
  2. Essa anotação personalizada em si pode ser combinada livremente para atingir vários objetivos.

escopo de feijão

Tipo de escopo Descrição do produto Usar
Singleton Crie uma instância de bean para todo o aplicativo Padrão
Protótipo Novas instâncias são criadas ao injetar ou quando obtidas através do contexto do aplicativo @Scope (ConfigurableBeanFactory.SCOPE_PROTOTYPE)
Conversação Aplicativo Web, crie uma instância para cada sessão @Scope (valor = WebApplicationContext.SCOPE_SESSION, proxyMode = 视 情况)
Pedido Aplicativo da Web, crie uma instância para cada solicitação @Scope (valor = WebApplicationContext.SCOPE_REQUEST, proxyMode = 视 情况)
//定义bean
@Configuration
public class HxDataSourceConfig {

    //单例bean。单例是spring bean的默认作用域
    @Bean
    public DriverManagerDataSource mysqlDataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/message?useSSL=false");
        datasource.setUsername("dev");
        datasource.setPassword("h123");
        return datasource;
    }

    //原型bean。使用点将得到一个新的bean实例
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Fruit someKindFruit() {
        Fruit f = new Fruit();
        return f;
    }
}

//验证
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = HxDataSourceConfig.class)
public class HxDataSourceConfigTest {
    @Autowired
    private DataSource dataSource;

    @Autowired
    private DataSource dataSource2;

    //pass,因为bean mysqlDataSource是单例,dataSource和dataSource2都指向该实例
    @Test
    public void testSingletonBean() {
        Assert.isTrue(dataSource == dataSource2);
    }

    @Autowired
    private Fruit fruit1;

    @Autowired
    private Fruit fruit2;

    //pass fruit1和fruit2指向不同的实例
    @Test
    public void testPrototypeBean() {
        Assert.isTrue(fruit1 != fruit2);
    }
}

Escopo de solicitação de sessão

Aqui se concentra na compreensão do conceito

 Como exemplo popular, todos nós já fomos a supermercados e carrinhos de compras usados. Se declararmos o carrinho de compras como um bean específico.
Para este bean, se houver apenas um carrinho de compras no supermercado, ele será chamado de singleton.Todas as
 vezes que formos ao supermercado, levaremos um carrinho de compras vazio, então isso é chamado de protótipo.
 Quanto às compras desta vez, achamos que é uma conversa, nessa conversa. Usamos
esse carrinho de compras, não importa em qual prateleira empurramos o carrinho de compras.
 Consideramos solicitados os sacos plásticos descartáveis ​​usados ​​para cada compra de diferentes categorias nessas compras: coloque um saco plástico ao comprar frutas e
outro saco plástico ao comprar produtos aquáticos.

    //session作用域的bean定义
    @Bean
    @Scope(value = WebApplicationContext.SCOPE_SESSION,
            proxyMode = ScopedProxyMode.INTERFACES)
    public ShorpCart someShopCart() {
        HandPushingShopCart cart = new HandPushingShopCart();
        return cart;
    }

 O problema que o proxyMode resolve aqui é: o problema quando os grãos com um ciclo de vida mais curto são injetados nos grãos com um ciclo de vida mais longo. Se o aplicativo injetar o ShopCart do bean de sessão no superMarket do bean singleton
, não haverá ShopCart com significado prático quando o bean singleton for inicializado. Então, o que é injetado no superMarker aqui é
o proxy do bean ShopCart . Quando o superMarket chama o método do bean ShopCart, a resolução do proxy delegará a chamada ao
bean ShopCart no escopo da sessão real.
  ScopedProxyMode.INTERFACES usado por proxyMode aqui, porque HandPushingShopCart implementa a
interface ShopCart, você pode usar o proxy padrão baseado na interface jdk. Se você não implementar a interface, poderá usar o proxy CGLIB, basta alterar para proxyMode = ScopedProxyMode.TARGET_CLASS

Como definir o escopo da sessão em xml

<bean id="someShopCart" class="com.hxapp.HandPushingShopCart"
          scope="session">
      <aop:scoped-proxy proxy-target-class="false"/>
</bean>

Injeção de valor de tempo de execução

Injetar valores externos

Geralmente, ele raramente é codificado no programa, considerando a flexibilidade.Geralmente, algumas variáveis ​​de atributo são definidas no arquivo de atributos, semelhante à maneira como
configuramos as variáveis ​​de ambiente do sistema.

//@PropertySource一般和@Configuration一起使用
//通过使用@PropertySource和Environment,指定属性文件里的属性及值就能被填充到Environment里
@Configuration
@PropertySource(value = "application.properties")
public class SomeConfig {
    @Autowired
    Environment env;

    @Bean
    public Student anyStudent() {
        if (env.containsProperty("student.name")) {
            return new Student(env.getProperty("student.name"));
        }
        return  new Student("");
    }
}

//测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SomeConfig.class)
public class RuntimeValueInjectionTest {

    @Autowired
    Student student;

    @Test
    public void testStudentNameFromProperties() {
        Assert.assertEquals("hyman", student.getName());
    }
}

O Spring 4.1 também fornece o uso de @TestPropertySource nos testes, a função é semelhante a @PropertySource, como mostrado abaixo

@ContextConfiguration(classes = HxDataSourceConfig.class) 
@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource("classpath:application.properties")
public class HxDataSourceConfigTest {

    @Autowired
    Environment env;

    @Test
    public void TestHymanName2() {
        String name = env.getProperty("student.name", String.class);
        assert(name.equals("hyman"));
    }
}

Use espaços reservados de atributo para injetar valores de atributo Os
arquivos de configuração XML usam espaços reservados na forma de $ {..} e precisam ser declarados<context:property-placeholder/>

<!--定义在resources目录下的some-config.xml-->
<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 https://www.springframework.org/schema/context/spring-context.xsd">
    <context:property-placeholder/>
    <bean id="student" class="com.hxapp.Student">
        <constructor-arg name="name" value="${student.name}"/>
    </bean>
</beans>
#定义在resources目录下的application.properties文件
student.name=hyman
//测试
@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource("classpath:application.properties")
@ContextConfiguration(locations = "classpath:some-config.xml")
public class RuntimeValueInjectionTest {
    @Autowired
    Student student;

    @Test
    public void testStudentNameFromProperties() {
        Assert.assertEquals("hyman", student.getName());
    }
}

A varredura de componentes e a fiação automática usam injeção de valor de bean, você pode usar @Value

//修改配置类,添加组件扫描组件,以扫描到student bean
@Configuration
@ComponentScan(basePackages = "com.hxapp")
public class SomeConfig {
}

//修改Student类,声明为组件,构造函数参数使用@Value
@Component
public class Student {
    String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    //使用@Value
    public Student(@Value("${student.name}")String name) {
        this.name = name;
    }
}

//测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SomeConfig.class)
@TestPropertySource("classpath:application.properties")
public class RuntimeValueInjectionTest {
    @Autowired
    Student student;

    @Test
    public void testStudentNameFromProperties() {
        Assert.assertEquals("hyman", student.getName());
    }
}

SpEL (idioma de expressão da primavera)

Formulário: # {Expression Body}
Introduzido desde o Spring 3
Use cenários: montagem de bean, regras de segurança do Spring, os modelos Thymeleaf usam SpEL para fazer referência aos dados do modelo

Características Exemplos
Executar operações aritméticas, relacionais e lógicas em valores # {T (java.lang.Math) .PI * R ^ 2} 、 # {1} 、 # {"123"} 、 # {score> 90? "bom mau"}
Bean de referência e seu método de atributo # {somebean} 、 # {somebean.property} 、 # {somebean.method ()}
Chamar métodos de objeto ou acessar propriedades # {systemProperties ['student.name']}
Suporte a expressão regular # {26characters.lowercase corresponde a [az]}
Conjunto de operação # {shelf.books [0] .title} 、 # {shelf.books.?[ititulo eq 'fogo e gelo']}

Para mais detalhes, consulte Spring IN Action Action Edition

Acho que você gosta

Origin www.cnblogs.com/hymanting/p/12726263.html
Recomendado
Clasificación