Spring annotations and XML configured in two ways IOC

Spring configuration of the two brothers of the IOC (XML & notes) single table CURD know

Previous article spent a lot of content, focusing on learning the Spring some ideas to get started, as well as simple to learn the IOC basis and based on XML-based configuration, we should be clear, XML and annotations are often inseparable, they are like twins but the idea of ​​two brothers are the same, that is to help developers achieve the desired functionality, we are talking IOC technology, no doubt in order to reduce the coupling between programs, then today to chat, annotation-based the IOC configuration, of course we have to learn contrast, two configurations at the same time to explain, and I have to try to complete some of the example, it comes complete with a single table CURD case

(A) preparation and environmental codes

(1) add the necessary dependencies

  • spring-context
  • mysql-connector-java
  • the c3p0 (database connection pool)
  • commons-dbutils (simplified JDBC tools) - back will be brief
  • JUnit (unit self-test)

Description: Since I created here is a Maven project, so here modify pom.xml to add the necessary dependencies can coordinate

Do not use a reliable friend, to download jar package into if we need to create it

<dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>

        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>

        <dependency>
            <groupId>commons-dbutils</groupId>
            <artifactId>commons-dbutils</artifactId>
            <version>1.4</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Simple look at some dependencies spring core, and database-related dependence on imported all came

(2) Create an account and entity table

A: Creating Account table

-- ----------------------------
-- Table structure for account
-- ----------------------------
CREATE TABLE `account`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32),
  `balance` float,
  PRIMARY KEY (`id`)
)

B: Creating the Account class

Nothing to say, corresponds to our table create entities

public class Account implements Serializable {
    private  Integer id;
    private String name;
    private Float balance;
    ......补充 get set toString 方法

(3) Creating Service and Dao

A: AccountService Interface

public interface AccountService {

    void add(Account account);

    void delete(Integer accpuntId);

    void update(Account account);

    List<Account> findAll();

    Account findById(Integer accountId);
}

B: AccountServiceImpl implementation class

public class AccountServiceImpl implements AccountService {

    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public void add(Account account) {
        accountDao.addAccount(account);
    }

    public void delete(Integer accpuntId) {
        accountDao.deleteAccount(accpuntId);
    }

    public void update(Account account) {
        accountDao.updateAccount(account);
    }

    public List<Account> findAll() {
        return accountDao.findAllAccount();
    }

    public Account findById(Integer accountId) {
        return accountDao.findAccountById(accountId);
    }
}

C: AccountDao Interface

public interface AccountDao {

    void addAccount(Account account);

    void deleteAccount(Integer accountId);

    void updateAccount(Account account);

    List<Account> findAllAccount();

    Account findAccountById(Integer accountId);
}

D: AccountDaoImpl implementation class

Because today the completion of a CRUD operations, so we introduced DBUtils such a tool for database operations, its role is to package code, to simplify JDBC operations purposes, due to the time after the integration of SSM framework, persistence layer things you can do to MyBatis, and today we focus is to explain the Spring of knowledge, so this chapter with it, look at the focus XML and annotations two configurations

Used to explain the basic contents:

QueryRunner sql statement provides an API for operation (insert delete update)

ResultSetHander interface After defining the query, how to package the result set (we only used)

  • BeanHander: The result set of the first record in the package to the specified JavaBean
  • BeanListHandler: all records of the result set to the specified JavaBean package, and each package to a JavaBean go List
public class AccountDaoImpl implements AccountDao {

    
    private QueryRunner runner;

    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }

    public void addAccount(Account account) {
        try {
            runner.update("insert into account(name,balance)values(?,?)", account.getName(), account.getBalance());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    public void updateAccount(Account account) {
        try {
            runner.update("update account set name=?,balance=? where id=?", account.getName(), account.getBalance(), account.getId());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void deleteAccount(Integer accountId) {
        try {
            runner.update("delete from account where id=?", accountId);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public List<Account> findAllAccount() {
        try {
            return runner.query("select * from account", new BeanListHandler<Account>(Account.class));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public Account findAccountById(Integer accountId) {
        try {
            return runner.query("select * from account where id = ? ", new BeanHandler<Account>(Account.class), accountId);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

(B) XML configuration

Here there are two basic ways, one is injected through the constructor, and the other is through the Set injection, actually did was, constructor or class assignment Set to member variables, but specifically, here it is by configuring, using the Spring framework is first injected into the head-dependent information, by the way, of course, we can find Quguan network posted over

First specific configuration code stickers above function

<?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">

    <!--配置Service-->
    <bean id="accountService" class="cn.ideal.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!--配置Dao-->
    <bean id="accountDao" class="cn.ideal.dao.impl.AccountDaoImpl">
        <property name="runner" ref="runner"></property>
    </bean>

    <!--配置 QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner">
        <!--注入数据源-->
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>

    <!--配置数据源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/ideal_spring"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root99"></property>

    </bean>
</beans>

Analyze:

Configuration Bean label, we saw the two forms of property, constructor-arg is set corresponding manner constructor form, Let me talk about the more common set way, with the code from above:

(1) Set mode

As the name suggests, is through to find you to give the corresponding Set method, then the member variable assignment, look at the code in the class

public class AccountServiceImpl implements AccountService {
    //成员
    private AccountDao accountDao;
    //Set方法
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    
    ...... 下面是增删改查的方法
}

This is bean.xml configuration

<!--配置Service-->
<bean id="accountService" class="cn.ideal.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"></property>
</bean>

The process then property configuration, there are some attributes need to talk about

  • name has nothing to do with members of the variable name, and the name of the set methods related to such setAccountDao () to get is accountDao, and have lower case at the beginning:
  • value : Here you can write basic data types and String
  • ref : Here you can introduce another bean, helping us to other types of assignments (for example, here following the introduction by ref id value accountDao of bean)

Of course, it could be seen way is to use the p-namespace injection data (nature or set)

头部中需要修改引入这一句

xmlns:p="http://www.springframework.org/schema/p"

我直接拿以前文章中的一个例子:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:p="http://www.springframework.org/schema/p"
    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">
 
<bean id="accountService" 
    class="cn.ideal.service.impl.AccountServiceImpl" p:name="汤姆" p:age="21" p:birthday-ref="nowdt"/>
<bean id="nowdt" class="java.util.Date"></bean>
</beans>

(2) 构造函数方式

下面就是使用构造函数的一种方式,这一种的前提就是:类中必须提供一个和参数列表相对应的构造函数

由于我们选择的是 DBUtils 这样一个工具,而它为我们提供了两种构造函数,即带参和无参,所以我们可以在其中注入数据源,也可以使得每一条语句都独立事务

还有一点需要说明的就是:我们下面的数据源使用了 c3p0 这只是一种选择方式,并不是一定的,是因为使用 DBUtils 的时候需要手动传递一个 Connection 对象

<!--配置 QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner">
    <!--注入数据源-->
    <constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>

来说一下所涉及到的标签:

  • constructor-arg(放在 bean 标签内) 再说一说其中的一些属性值
    • 给谁赋值:
      • index:指定参数在构造函数参数列表的索引位置
      • type:指定参数在构造函数中的数据类型
      • name:指定参数在构造函数中的名称(更常用)
    • 赋什么值:
      • value:这里可以写基本数据类型和 String
      • ref:这里可以引入另一个bean,帮助我们给其他类型赋值

(3) 注入集合属性

为了演示这些方式,我们在一个实体成员中将常见的一些集合都写出来,然后补充其 set 方法

private String[] strs;
private List<String> list;
private Set<String> set;
private Map<String,String> map;
private Properties props;

在配置中也是很简单的,只需要按照下列格式写标签就可以了,由于增删改查中并没有涉及到集合的相关信息,这里就是简单提一下,可以自己测试一下

<bean id="accountService" class="cn.ideal.service.impl.AccountServiceImpl">
    <property name="strs">
        <array>
            <value>张三</value>
            <value>李四</value>
            <value>王五</value>
        </array>
    </property>

    <property name="list">
        <list>
            <value>张三</value>
            <value>李四</value>
            <value>王五</value>
        </list>
    </property>

    <property name="set">
        <set>
            <value>张三</value>
            <value>李四</value>
            <value>王五</value>
        </set>
    </property>

    <property name="map">
        <map>
            <entry key="name" value="张三"></entry>
            <entry key="age" value="21"></entry>
        </map>
    </property>

    <property name="props">
        <props>
            <prop key="name">张三</prop>
            <prop key="age">21</prop>
        </props>
    </property>
 </bean>

(4) 测试代码

public class AccountServiceTest {

    private ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
    private AccountService as = ac.getBean("accountService", AccountService.class);

    @Test
    public void testAdd(){
        Account account = new Account();
        account.setName("jack");
        account.setBalance(1000f);
        as.add(account);
    }

    @Test
    public void testUpdate(){
        Account account = as.findById(4);
        account.setName("杰克");
        account.setBalance(1500f);
        as.update(account);
    }

    @Test
    public void testFindAll(){
        List<Account> list = as.findAll();
        for(Account account : list) {
            System.out.println(account);
        }
    }

    @Test
    public void testDelete(){
        as.delete(4);
    }
}

(5) 执行效果

添加,修改(包含了查询指定id),删除

查询所有

(三) 注解配置方式

首先,我们先将上面的例子使用注解来实现一下,再来具体的讲解:

(1) 改造原程序为注解配置

首先需要为 Dao 和 Service 的实现类中 添加注解

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
    
    下面的原封不动
}
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
    @Autowired
    private QueryRunner runner;
   
    下面的原封不动
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">
    <!--开启扫描-->
    <context:component-scan base-package="cn.ideal"></context:component-scan>

    <!--配置 QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner">
        <!--注入数据源-->
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>

    <!--配置数据源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/ideal_spring"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root99"></property>
    </bean>
</beans>

到这里,一个最基本的注解改造就完成了,大家可以用前面的测试类进行一下测试

下面我们正式说一下注解配置相关的知识

(2) 常用注解

A:创建对象

@Component

  • 让Spring 来管理资源,相当于XML 中配置一个 bean <bean id="" class="">
  • 可以在括号内指定 value 值,即指定 bean 的 id ,如果不指定就会默认的使用当前类的类名
  • 如果注解中只有一个value属性要赋值,value可以不写,直接写名称,如上面例子中

@Controller @Service @Repository

对于创建对象的注解,Spring 还提供了三种更加明确的说法,作用是完全相同的,但是针对不同的场景起了不同的叫法罢了

  • @Controller:一般用于表现层
  • @Service:一般用于业务层
  • @Repository:一般用于持久层

B:注入数据

@Autowired
  • 自动按类型注入,相当于XML 中配置一个 bean <property name="" ref=""> 或者 <property name="" value="">

  • 容器中有一个唯一的 bean 对象类型和注入的变量类型一致,则注入成功

    • @Autowired
      private AccountDao accountDao;
      
      @Repository("accountDao")
      public class AccountDaoImpl implements AccountDao {......}
    • 比如上面的例子,Spring的IOC中容器是一个Map的结构,字符串“accountDao” 以及这个可以认为是 AccountDao 类型的 AccountDaoImpl 类就被以键值对的形式存起来,被注解 @Autowired的地方,会直接去容器的 value 部分去找 AccountDao 这个类型的类

    • 当 IoC 中匹配到了多个符合的,就会根据变量名去找,找不到则报错:例如下面,根据 AccountDao类型匹配到了两个类,所以根据变量名去找找到了 AccountDaoImplA 这个类

    • @Autowired
      private AccountDao accountDaoA;
      
      @Repository("accountDaoA")
      public class AccountDaoImplA implements AccountDao {......}
      
      @Repository("accountDaoB")
      public class AccountDaoImplB implements AccountDao {......}
  • 可以对类的成员变量、方法以及构造函数进行标注,完成自动装配

  • 使用此注解可以省略 set 方法

@Qualifier
  • 在自动按类型注入的基础之上,按照 Bean 的 id 注入,给字段注入的时候不能够单独使用,需要配合上面的 @Autiwire 使用,但是给方法参数注入的时候,可以独立使用
  • 使用时:value 值指定 bean 的 id

它有时候必须配合别的注解使用,有没有一个标签可以解决这个问题呢?答案就是 @Resource

@Resource
  • 直接按照 bean 的 id 注入,不过只能注入其他 bean 类型
  • 使用时:name 值指定 bean 的 id

前面三个都是用来注入其他的 bean 类型的数据,下面来说一说,基本类型以及String的实现

(特别说明:集合类型的注入只能通过 XML 来实现)

@Value
  • 这个注解就是用来注入基本数据类型和 String 类型数据的
  • 使用时:value 属性用于指定值

C:改变作用范围

@Scope
  • 指定 bean 的作用范围 相当于XML 中配置一个 <bean id="" class="" scope
  • 使用时:value 属性用于指定范围的值(singleton prototype request session globalsession)

D:生命周期相关

相当于:<bean id="" class="" init-method="" destroy-method="" />

@PostConstruct
  • 指定初始化方法
@PreDestroy
  • 指定销毁方法

(四) XML和注解 的对比与选择

(1) 优缺点

一般来说,我们两种配置方式都是有人使用的,不过我个人更习惯使用注解的方式

  • XML
    • 类之间的松耦合关系,扩展性强,利于更换修改
    • 对象之间的关系清晰明了
  • 注解:
    • 简化配置,并且使用起来也容易,效率会高一些
    • 在类中就能找对配置,清晰明了
    • 类型安全

(2) 两者对比

XML配置 注解配置
创建对象 <bean id="" class=""> @Controller @Service @Repository@Component
指定名称 通过 id 或者 name 值指定 @Controller("指定的名称")
注入数据 <property name="" ref=""> @Autowired @Qualifier @Resource @Value
作用范围 <bean id="" class="" scope> @Scope
生命周期 <bean id="" class="" init-method="" destroy-method=""/> @PostConstruct @PreDestroy

(五) 补充新注解

为什么要补充新注解呢? 在我们使用注解时,在书写代码时,简化了很多,但是我们在 bean.xml 文件中 仍然需要 开启扫描、 进行配置QueryRunner 以及 数据源,如何彻底摆脱 xml 配置全面使用注解呢?

这也就是我们将要补充的几个新注解,作用就是让我们全面使用注解进行开发

(1) 注解讲解

A: 配置类注解

@Configuration
  • 指定当前类是 spring 的一个配置类,相当于 XML中的 bean.xml 文件

  • 获取容器时需要使用下列形式

  • private ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);

依旧使用上方的 CURD 的案例代码进行修改,首先与cn同级创建了一个名为 config 的包,然后编写一个名为 SpringConfiguration 的类,当然实际上这两个名字无所谓的,添加注解

@Configuration
public class SpringConfiguration {
}

B: 指定扫描包注解

@ComponentScan

@Configuration 相当于已经帮我们把 bean.xml 文件创立好了,按照我们往常的步骤,应该指定扫描的包了,这也就是我们这个注解的作用

  • 指定 spring 在初始化容器时要扫描的包,在 XML 中相当于:

  • <!--开启扫描-->
    <context:component-scan base-package="cn.ideal"></context:component-scan>
  • 其中 basePackages 用于指定扫描的包,和这个注解中value属性的作用是一致的

具体使用:

@Configuration
@ComponentScan("cn.ideal")
public class SpringConfiguration {
}

C: 创建对象

@Bean

写好了配置类,以及指定了扫描的包,下面该做的就是配置 QueryRunner 以及数据源了,在 XML 中我们会通过书写 bean 标签来配置,而 Spring 为我们提供了 @Bean 这个注解来替代原来的标签

  • 将注解写在方法上(只能是方法),也就是代表用这个方法创建一个对象,然后放到 Spring 的容器中去
  • 通过 name 属性 给这个方法指定名称,也就是我们 XML 中 bean 的 id

具体使用:

package config;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.apache.commons.dbutils.QueryRunner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class JdbcConfig {

    /**
     * 创建一个 QueryRunner对象
     * @param dataSource
     * @return
     */
    @Bean(name = "runner")
    public QueryRunner creatQueryRunner(DataSource dataSource){
        return  new QueryRunner(dataSource);
    }


    /**
     * 创建数据源,并且存入spring
     * @return
     */
    @Bean(name = "dataSource")
    public DataSource createDataSource() {
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource();
            ds.setUser("root");
            ds.setPassword("1234");
            ds.setDriverClass("com.mysql.jdbc.Driver");
            ds.setJdbcUrl("jdbc:mysql:///spring_day02");
            return ds;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

D: 配置 properties 文件

@PropertySource

上面在创建数据源的时候,都是直接把配置信息写死了,如果想要使用 properties 进行内容的配置,在这时候就需要,使用 @PropertySource 这个注解

  • 用于加载 .properties 文件中的配置
  • value [] 指定 properties 文件位置,在类路径下,就需要加上 classpath
@Configuration
@ComponentScan("cn.ideal")
@PropertySource("classpath:jdbcConfig.properties")
public class SpringConfiguration {
}
public class JdbcConfig {

    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;


    /**
     * 创建一个 QueryRunner对象
     * @param dataSource
     * @return
     */
    @Bean(name = "runner")
    public QueryRunner creatQueryRunner(DataSource dataSource){
        return  new QueryRunner(dataSource);
    }


    /**
     * 创建数据源,并且存入spring
     * @return
     */
    @Bean(name = "dataSource")
    public DataSource createDataSource() {
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource();
            ds.setUser(username);
            ds.setPassword(password);
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            return ds;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

E:导入其他配置类

@Import

这样看来一个 JdbcConfig 就基本写好了,我们在其中配置了 QueryRunner 对象,以及数据源,这个时候,实际上我们原先的 bean.xml 就可以删掉了,但是我们虽然写好了 JdbcConfig 但是如何将两个配置文件联系起来呢?这也就是这个注解的作用

@Configuration
@ComponentScan("cn.ideal")
@Import(JdbcConfig.class)
@PropertySource("classpath:jdbcConfig.properties")
public class SpringConfiguration {
}

(2) 注解获取容器

修改获取容器的方式后,就可以进行测试了

private ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
private AccountService as = ac.getBean("accountService", AccountService.class);

(五) Spring 单元测试改进

由于我们需要通过上面测试中两行代获取到容器,为了不每一次都写这两行代码,所以我们在前面将其定义在了成员位置,但是有没有办法可以省掉这个步骤呢?

也就是说,我们想要程序自动创建容器,但是原来的 junit 很显然是实现不了的,因为它并不会知道我们是否使用了 spring ,不过 junit 提供了一个注解让我们替换它的运行器,转而由 spring 提供

首先需要导入 jar 包 或者说导入依赖坐标

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

使用 @RunWith 注解替换原有运行器 然后使用 @ContextConfiguration 指定 spring 配置文件的位置,然后使用 @Autowired 给变量注入数据

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class AccountServiceTest {

    @Autowired
    private AccountService as;
    
}

(六) 总结

到这里,这篇文章就结束了,这篇文章主要讲解的内容接着我们上一篇文章讲解,上一篇更多的是从传统循序渐进的提到 Spring,更多的是帮助大家理解为什么用Spring,而这篇文章是基于上一篇的基础,更多的提到的是如何去用 Spring,所以看起来更像是一个工具书一样的文章,并没有提到很多的思想原理,而是帮助大家快速的上手用 XML 以及注解的方式,当然大神自然很多,不敢说什么有技术,但总归是希望能给不太熟悉 spring 的朋友一些帮助,或者临时当做一篇工具文来查找,再次感谢大家的访问与赞,谢谢朋友们的支持!再次感谢!

(七) 结尾

如果文章中有什么不足,欢迎大家留言交流,感谢朋友们的支持!

如果能帮到你的话,那就来关注我吧!如果您更喜欢微信文章的阅读方式,可以关注我的公众号

在这里的我们素不相识,却都在为了自己的梦而努力 ❤

一个坚持推送原创开发技术文章的公众号:理想二旬不止

Guess you like

Origin www.cnblogs.com/ideal-20/p/12357188.html