30个类手写Spring核心原理之自定义ORM(上)(6)

30个类手写Spring核心原理之自定义ORM(上)(6)

本文节选自《Spring 5核心原理》

1 实现思路概述#

1.1 从ResultSet说起#

说到ResultSet,有Java开发经验的“小伙伴”自然最熟悉不过了,不过我相信对于大多数人来说也算是“最熟悉的陌生人”。从ResultSet取值操作大家都会,比如:

private static List<Member> select(String sql) {    List<Member> result = new ArrayList<>();    Connection con = null;    PreparedStatement pstm = null;    ResultSet rs = null;    try {        //1. 加载驱动类        Class.forName("com.mysql.jdbc.Driver");        //2. 建立连接        con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/gp-vip-spring-db-demo", "root","123456");        //3. 创建语句集        pstm =  con.prepareStatement(sql);        //4. 执行语句集        rs = pstm.executeQuery();        while (rs.next()){            Member instance = new Member();            instance.setId(rs.getLong("id"));            instance.setName(rs.getString("name"));            instance.setAge(rs.getInt("age"));            instance.setAddr(rs.getString("addr"));            result.add(instance);        }        //5. 获取结果集    }catch (Exception e){        e.printStackTrace();    }    //6. 关闭结果集、关闭语句集、关闭连接    finally {        try {            rs.close();            pstm.close();            con.close();        }catch (Exception e){            e.printStackTrace();        }    }    return result;}

以上我们在没有使用框架以前的常规操作。随着业务和开发量的增加,在数据持久层这样的重复代码出现频次非常高。因此,我们就想到将非功能性代码和业务代码进行分离。我们首先想到将ResultSet封装数据的代码逻辑分离,增加一个mapperRow()方法,专门处理对结果的封装,代码如下:

private static List<Member> select(String sql) {    List<Member> result = new ArrayList<>();    Connection con = null;    PreparedStatement pstm = null;    ResultSet rs = null;    try {        //1. 加载驱动类        Class.forName("com.mysql.jdbc.Driver");        //2. 建立连接        con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/gp-vip-spring-db-demo", "root","123456");        //3. 创建语句集        pstm =  con.prepareStatement(sql);        //4. 执行语句集        rs = pstm.executeQuery();        while (rs.next()){            Member instance = mapperRow(rs,rs.getRow());            result.add(instance);        }        //5. 获取结果集    }catch (Exception e){        e.printStackTrace();    }    //6. 关闭结果集、关闭语句集、关闭连接    finally {        try {            rs.close();            pstm.close();            con.close();        }catch (Exception e){            e.printStackTrace();        }    }    return result;}private static Member mapperRow(ResultSet rs, int i) throws Exception {    Member instance = new Member();    instance.setId(rs.getLong("id"));    instance.setName(rs.getString("name"));    instance.setAge(rs.getInt("age"));    instance.setAddr(rs.getString("addr"));    return instance;}

但在真实的业务场景中,这样的代码逻辑重复率实在太高,上面的改造只能应用Member类,换一个实体类又要重新封装,聪明的程序员肯定不会通过纯体力劳动给每一个实体类写一个mapperRow()方法,一定会想到代码复用方案。我们不妨来做这样一个改造。
先创建Member类:

package com.gupaoedu.vip.orm.demo.entity;import lombok.Data;import javax.persistence.Entity;import javax.persistence.Id;import javax.persistence.Table;import java.io.Serializable;@Entity@Table(name="t_member")@Datapublic class Member implements Serializable {    @Id private Long id;    private String name;    private String addr;    private Integer age;    @Override    public String toString() {        return "Member{" +                "id=" + id +                ", name='" + name + '\'' +                ", addr='" + addr + '\'' +                ", age=" + age +                '}';    }}

优化JDBC操作:

public static void main(String[] args) {    Member condition = new Member();    condition.setName("Tom");    condition.setAge(19);    List<?> result =  select(condition);    System.out.println(Arrays.toString(result.toArray()));}private static List<?> select(Object condition) {    List<Object> result = new ArrayList<>();    Class<?> entityClass = condition.getClass();    Connection con = null;    PreparedStatement pstm = null;    ResultSet rs = null;    try {        //1. 加载驱动类        Class.forName("com.mysql.jdbc.Driver");        //2. 建立连接        con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/gp-vip-spring-db-demo? characterEncoding=UTF-8&rewriteBatchedStatements=true","root","123456");        //根据类名找属性        Map<String,String> columnMapper = new HashMap<String,String>();        //根据属性名找字段名        Map<String,String> fieldMapper = new HashMap<String,String>();        Field[] fields =  entityClass.getDeclaredFields();        for (Field field : fields) {            field.setAccessible(true);            String fieldName = field.getName();            if(field.isAnnotationPresent(Column.class)){                Column column = field.getAnnotation(Column.class);                String columnName = column.name();                columnMapper.put(columnName,fieldName);                fieldMapper.put(fieldName,columnName);            }else {                //默认就是字段名、属性名一致                columnMapper.put(fieldName, fieldName);                fieldMapper.put(fieldName,fieldName);            }        }        //3. 创建语句集        Table table = entityClass.getAnnotation(Table.class);        String sql = "select * from " + table.name();        StringBuffer where = new StringBuffer(" where 1=1 ");        for (Field field : fields) {            Object value =field.get(condition);            if(null != value){                if(String.class == field.getType()) {                    where.append(" and " + fieldMapper.get(field.getName()) + " = '" + value + "'");                }else{                    where.append(" and " + fieldMapper.get(field.getName()) + " = " + value + "");                }                //其他的在这里就不一一列举,后面我们手写ORM框架时会完善            }        }        System.out.println(sql + where.toString());        pstm =  con.prepareStatement(sql + where.toString());        //4. 执行语句集        rs = pstm.executeQuery();        //元数据?        //保存了处理真正数值以外的所有附加信息        int columnCounts = rs.getMetaData().getColumnCount();        while (rs.next()){            Object instance = entityClass.newInstance();            for (int i = 1; i <= columnCounts; i++) {                //实体类属性名,对应数据库表的字段名                //可以通过反射机制拿到实体类的所有字段                //从rs中取得当前这个游标下的类名                String columnName = rs.getMetaData().getColumnName(i);                //有可能是私有的                Field field = entityClass.getDeclaredField(columnMapper.get(columnName));                field.setAccessible(true);                field.set(instance,rs.getObject(columnName));            }            result.add(instance);        }        //5. 获取结果集    }catch (Exception e){        e.printStackTrace();    }    //6. 关闭结果集、关闭语句集、关闭连接    finally {        try {            rs.close();            pstm.close();            con.close();        }catch (Exception e){            e.printStackTrace();        }    }    return result;}

上面巧妙地利用反射机制读取Class信息和Annotation信息,将数据库表中的列和类中的字段进行关联映射并赋值,以减少重复代码。

1.2 为什么需要ORM框架#

通过前面的讲解,我们已经了解ORM框架的基本实现原理。ORM是指对象关系映射(Object Relation Mapping),映射的不只是对象值,还有对象与对象之间的关系,例如一对多、多对多、一对一这样的表关系。现在市面上ORM框架也非常多,有大家所熟知的Hibernate、Spring JDBC、MyBatis、JPA等。在这里做一个简单的总结,如下表所示。

名称特征描述Hibernate全自动(挡)不需要写一句SQLMyBatis半自动(挡)手自一体,支持简单的映射,复杂关系需要自己写SQLSpring JDBC纯手动(挡)所有的SQL都要自己写,它帮我们设计了一套标准流程

既然市面上有这么多选择,我为什么还要自己写 ORM框架呢?
这得从我的一次空降担任架构师的经验说起。空降面临最大的难题就是如何取得团队“小伙伴们”的信任。当时,团队总共就8人,每个人的水平参差不齐,甚至有些人还没接触过MySQL,诸如Redis等缓存中间件更不用说了。基本只会使用Hibernate的CRUD,而且已经影响到了系统性能。由于工期紧张,没有时间和精力给团队做系统培训,也为了兼顾可控性,于是就产生了自研ORM框架的想法。我做了这样的顶层设计,以降低团队“小伙伴们”的存息成本,顶层接口统一参数、统一返回值,具体如下。

**(1)规定查询方法的接口模型为: **

/** * 获取列表 * @param queryRule 查询条件 * @return */List<T> select(QueryRule queryRule) throws Exception;/** * 获取分页结果 * @param queryRule 查询条件 * @param pageNo 页码 * @param pageSize 每页条数 * @return */Page<?> select(QueryRule queryRule,int pageNo,int pageSize) throws Exception;/** * 根据SQL获取列表 * @param sql SQL语句 * @param args 参数 * @return */List<Map<String,Object>> selectBySql(String sql, Object... args) throws Exception;/** * 根据SQL获取分页 * @param sql SQL语句 * @param pageNo 页码 * @param pageSize 每页条数 * @return */Page<Map<String,Object>> selectBySqlToPage(String sql, Object [] param, int pageNo, int pageSize) throws Exception;

(2)规定删除方法的接口模型为:

/** * 删除一条记录 * @param entity entity中的ID不能为空,如果ID为空,其他条件不能为空,都为空不予执行 * @return */boolean delete(T entity) throws Exception;/** * 批量删除 * @param list * @return 返回受影响的行数 * @throws Exception */int deleteAll(List<T> list) throws Exception;

(3)规定插入方法的接口模型为:

/** * 插入一条记录并返回插入后的ID * @param entity 只要entity不等于null,就执行插入 * @return */PK insertAndReturnId(T entity) throws Exception;/** * 插入一条记录自增ID * @param entity * @return * @throws Exception */boolean insert(T entity) throws Exception;/** * 批量插入 * @param list * @return 返回受影响的行数 * @throws Exception */int insertAll(List<T> list) throws Exception;

(4)规定修改方法的接口模型为:

/** *  修改一条记录 * @param entity entity中的ID不能为空,如果ID为空,其他条件不能为空,都为空不予执行 * @return * @throws Exception */boolean update(T entity) throws Exception;

利用这套基础的API,后面我又基于Redis、MongoDB、ElasticSearch、Hive、HBase各封装了一套,以此来降低团队的学习成本,也大大提升了程序的可控性,更方便统一监控。

2 搭建基础架构#

2.1 Page#

定义Page类的主要目的是为后面的分页查询统一返回结果做顶层支持,其主要功能包括分页逻辑的封装、分页数据。

package javax.core.common;import java.io.Serializable;import java.util.ArrayList;import java.util.List;/** * 分页对象,包含当前页数据及分页信息,如总记录数 * 能够支持和JQuery EasyUI直接对接,能够支持和BootStrap Table直接对接 */public class Page<T> implements Serializable {   private static final long serialVersionUID = 1L;   private static final int DEFAULT_PAGE_SIZE = 20;   private int pageSize = DEFAULT_PAGE_SIZE; //每页的记录数   private long start; //当前页第一条数据在List中的位置,从0开始   private List<T> rows; //当前页中存放的记录,类型一般为List   private long total; //总记录数   /**    * 构造方法,只构造空页    */   public Page() {      this(0, 0, DEFAULT_PAGE_SIZE, new ArrayList<T>());   }   /**    * 默认构造方法    *     * @param start 本页数据在数据库中的起始位置    * @param totalSize 数据库中总记录条数    * @param pageSize 本页容量    * @param rows 本页包含的数据    */   public Page(long start, long totalSize, int pageSize, List<T> rows) {      this.pageSize = pageSize;      this.start = start;      this.total = totalSize;      this.rows = rows;   }   /**    * 取总记录数    */   public long getTotal() {      return this.total;   }      public void setTotal(long total) {      this.total = total;   }   /**    * 取总页数    */   public long getTotalPageCount() {      if (total % pageSize == 0){         return total / pageSize;      }else{         return total / pageSize + 1;      }   }   /**    * 取每页数据容量    */   public int getPageSize() {      return pageSize;   }   /**    * 取当前页中的记录    */   public List<T> getRows() {      return rows;   }      public void setRows(List<T> rows) {      this.rows = rows;   }   /**    * 取该页的当前页码,页码从1开始    */   public long getPageNo() {      return start / pageSize + 1;   }   /**    * 该页是否有下一页    */   public boolean hasNextPage() {      return this.getPageNo() < this.getTotalPageCount() - 1;   }   /**    * 该页是否有上一页    */   public boolean hasPreviousPage() {      return this.getPageNo() > 1;   }   /**    * 获取任意一页第一条数据在数据集中的位置,每页条数使用默认值    *     * @see #getStartOfPage(int,int)    */   protected static int getStartOfPage(int pageNo) {      return getStartOfPage(pageNo, DEFAULT_PAGE_SIZE);   }   /**    * 获取任意一页第一条数据在数据集中的位置    *     * @param pageNo 从1开始的页号    * @param pageSize 每页记录条数    * @return 该页第一条数据    */   public static int getStartOfPage(int pageNo, int pageSize) {      return (pageNo - 1) * pageSize;   }}

2.2 ResultMsg#

ResultMsg类主要是为统一返回结果做的顶层设计,主要包括状态码、结果说明内容和返回数据。

package javax.core.common;import java.io.Serializable;//底层设计public class ResultMsg<T> implements Serializable {   private static final long serialVersionUID = 2635002588308355785L;   private int status; //状态码,系统的返回码   private String msg;  //状态码的解释   private T data;  //放任意结果   public ResultMsg() {}      public ResultMsg(int status) {      this.status = status;   }   public ResultMsg(int status, String msg) {      this.status = status;      this.msg = msg;   }      public ResultMsg(int status, T data) {      this.status = status;      this.data = data;   }   public ResultMsg(int status, String msg, T data) {      this.status = status;      this.msg = msg;      this.data = data;   }   public int getStatus() {      return status;   }   public void setStatus(int status) {      this.status = status;   }   public String getMsg() {      return msg;   }   public void setMsg(String msg) {      this.msg = msg;   }   public T getData() {      return data;   }   public void setData(T data) {      this.data = data;   }}

2.3 BaseDao#

作为所有BaseDao持久化框架的顶层接口,主要定义增、删、改、查统一的参数列表和返回值。

package javax.core.common.jdbc;import com.gupaoedu.vip.orm.framework.QueryRule;import javax.core.common.Page;import java.util.List;import java.util.Map;public interface BaseDao<T,PK> {    /**     * 获取列表     * @param queryRule 查询条件     * @return     */    List<T> select(QueryRule queryRule) throws Exception;    /**     * 获取分页结果     * @param queryRule 查询条件     * @param pageNo 页码     * @param pageSize 每页条数     * @return     */    Page<?> select(QueryRule queryRule,int pageNo,int pageSize) throws Exception;    /**     * 根据SQL获取列表     * @param sql SQL语句     * @param args 参数     * @return     */    List<Map<String,Object>> selectBySql(String sql, Object... args) throws Exception;    /**     * 根据SQL获取分页     * @param sql SQL语句     * @param pageNo 页码     * @param pageSize 每页条数     * @return     */    Page<Map<String,Object>> selectBySqlToPage(String sql, Object [] param, int pageNo, int pageSize) throws Exception;    /**     * 删除一条记录     * @param entity entity中的ID不能为空,如果ID为空,其他条件不能为空,都为空则不予执行     * @return     */    boolean delete(T entity) throws Exception;    /**     * 批量删除     * @param list     * @return 返回受影响的行数     * @throws Exception     */    int deleteAll(List<T> list) throws Exception;    /**     * 插入一条记录并返回插入后的ID     * @param entity 只要entity不等于null,就执行插入操作     * @return     */    PK insertAndReturnId(T entity) throws Exception;    /**     * 插入一条记录自增ID     * @param entity     * @return     * @throws Exception     */    boolean insert(T entity) throws Exception;    /**     * 批量插入     * @param list     * @return 返回受影响的行数     * @throws Exception     */    int insertAll(List<T> list) throws Exception;    /**     *  修改一条记录     * @param entity entity中的ID不能为空,如果ID为空,其他条件不能为空,都为空则不予执行     * @return     * @throws Exception     */    boolean update(T entity) throws Exception;}

2.4 QueryRule#

如果用QueryRule类来构建查询条件,用户在做条件查询时不需要手写SQL,实现业务代码与SQL解耦。

package com.gupaoedu.vip.orm.framework;import java.io.Serializable;import java.util.ArrayList;import java.util.List;/** * QueryRule,主要功能用于构造查询条件 */public final class QueryRule implements Serializable{   private static final long serialVersionUID = 1L;   public static final int ASC_ORDER = 101;   public static final int DESC_ORDER = 102;   public static final int LIKE = 1;   public static final int IN = 2;   public static final int NOTIN = 3;   public static final int BETWEEN = 4;   public static final int EQ = 5;   public static final int NOTEQ = 6;   public static final int GT = 7;   public static final int GE = 8;   public static final int LT = 9;   public static final int LE = 10;   public static final int ISNULL = 11;   public static final int ISNOTNULL = 12;   public static final int ISEMPTY = 13;   public static final int ISNOTEMPTY = 14;   public static final int AND = 201;   public static final int OR = 202;   private List<Rule> ruleList = new ArrayList<Rule>();   private List<QueryRule> queryRuleList = new ArrayList<QueryRule>();   private String propertyName;   private QueryRule() {}   private QueryRule(String propertyName) {      this.propertyName = propertyName;   }   public static QueryRule getInstance() {      return new QueryRule();   }      /**    * 添加升序规则    * @param propertyName    * @return    */   public QueryRule addAscOrder(String propertyName) {      this.ruleList.add(new Rule(ASC_ORDER, propertyName));      return this;   }   /**    * 添加降序规则    * @param propertyName    * @return    */   public QueryRule addDescOrder(String propertyName) {      this.ruleList.add(new Rule(DESC_ORDER, propertyName));      return this;   }   public QueryRule andIsNull(String propertyName) {      this.ruleList.add(new Rule(ISNULL, propertyName).setAndOr(AND));      return this;   }   public QueryRule andIsNotNull(String propertyName) {      this.ruleList.add(new Rule(ISNOTNULL, propertyName).setAndOr(AND));      return this;   }   public QueryRule andIsEmpty(String propertyName) {      this.ruleList.add(new Rule(ISEMPTY, propertyName).setAndOr(AND));      return this;   }   public QueryRule andIsNotEmpty(String propertyName) {      this.ruleList.add(new Rule(ISNOTEMPTY, propertyName).setAndOr(AND));      return this;   }   public QueryRule andLike(String propertyName, Object value) {      this.ruleList.add(new Rule(LIKE, propertyName, new Object[] { value }).setAndOr(AND));      return this;   }   public QueryRule andEqual(String propertyName, Object value) {      this.ruleList.add(new Rule(EQ, propertyName, new Object[] { value }).setAndOr(AND));      return this;   }   public QueryRule andBetween(String propertyName, Object... values) {      this.ruleList.add(new Rule(BETWEEN, propertyName, values).setAndOr(AND));      return this;   }   public QueryRule andIn(String propertyName, List<Object> values) {      this.ruleList.add(new Rule(IN, propertyName, new Object[] { values }).setAndOr(AND));      return this;   }   public QueryRule andIn(String propertyName, Object... values) {      this.ruleList.add(new Rule(IN, propertyName, values).setAndOr(AND));      return this;   }      public QueryRule andNotIn(String propertyName, List<Object> values) {      this.ruleList.add(new Rule(NOTIN, propertyName, new Object[] { values }).setAndOr(AND));      return this;   }   public QueryRule orNotIn(String propertyName, Object... values) {      this.ruleList.add(new Rule(NOTIN, propertyName, values).setAndOr(OR));      return this;   }      public QueryRule andNotEqual(String propertyName, Object value) {      this.ruleList.add(new Rule(NOTEQ, propertyName, new Object[] { value }).setAndOr(AND));      return this;   }   public QueryRule andGreaterThan(String propertyName, Object value) {      this.ruleList.add(new Rule(GT, propertyName, new Object[] { value }).setAndOr(AND));      return this;   }   public QueryRule andGreaterEqual(String propertyName, Object value) {      this.ruleList.add(new Rule(GE, propertyName, new Object[] { value }).setAndOr(AND));      return this;   }   public QueryRule andLessThan(String propertyName, Object value) {      this.ruleList.add(new Rule(LT, propertyName, new Object[] { value }).setAndOr(AND));      return this;   }   public QueryRule andLessEqual(String propertyName, Object value) {      this.ruleList.add(new Rule(LE, propertyName, new Object[] { value }).setAndOr(AND));      return this;   }         public QueryRule orIsNull(String propertyName) {      this.ruleList.add(new Rule(ISNULL, propertyName).setAndOr(OR));      return this;   }   public QueryRule orIsNotNull(String propertyName) {      this.ruleList.add(new Rule(ISNOTNULL, propertyName).setAndOr(OR));      return this;   }   public QueryRule orIsEmpty(String propertyName) {      this.ruleList.add(new Rule(ISEMPTY, propertyName).setAndOr(OR));      return this;   }   public QueryRule orIsNotEmpty(String propertyName) {      this.ruleList.add(new Rule(ISNOTEMPTY, propertyName).setAndOr(OR));      return this;   }   public QueryRule orLike(String propertyName, Object value) {      this.ruleList.add(new Rule(LIKE, propertyName, new Object[] { value }).setAndOr(OR));      return this;   }   public QueryRule orEqual(String propertyName, Object value) {      this.ruleList.add(new Rule(EQ, propertyName, new Object[] { value }).setAndOr(OR));      return this;   }   public QueryRule orBetween(String propertyName, Object... values) {      this.ruleList.add(new Rule(BETWEEN, propertyName, values).setAndOr(OR));      return this;   }   public QueryRule orIn(String propertyName, List<Object> values) {      this.ruleList.add(new Rule(IN, propertyName, new Object[] { values }).setAndOr(OR));      return this;   }   public QueryRule orIn(String propertyName, Object... values) {      this.ruleList.add(new Rule(IN, propertyName, values).setAndOr(OR));      return this;   }   public QueryRule orNotEqual(String propertyName, Object value) {      this.ruleList.add(new Rule(NOTEQ, propertyName, new Object[] { value }).setAndOr(OR));      return this;   }   public QueryRule orGreaterThan(String propertyName, Object value) {      this.ruleList.add(new Rule(GT, propertyName, new Object[] { value }).setAndOr(OR));      return this;   }   public QueryRule orGreaterEqual(String propertyName, Object value) {      this.ruleList.add(new Rule(GE, propertyName, new Object[] { value }).setAndOr(OR));      return this;   }   public QueryRule orLessThan(String propertyName, Object value) {      this.ruleList.add(new Rule(LT, propertyName, new Object[] { value }).setAndOr(OR));      return this;   }   public QueryRule orLessEqual(String propertyName, Object value) {      this.ruleList.add(new Rule(LE, propertyName, new Object[] { value }).setAndOr(OR));      return this;   }      public List<Rule> getRuleList() {      return this.ruleList;   }   public List<QueryRule> getQueryRuleList() {      return this.queryRuleList;   }   public String getPropertyName() {      return this.propertyName;   }   protected class Rule implements Serializable {      private static final long serialVersionUID = 1L;      private int type;  //规则的类型      private String property_name;      private Object[] values;      private int andOr = AND;      public Rule(int paramInt, String paramString) {         this.property_name = paramString;         this.type = paramInt;      }      public Rule(int paramInt, String paramString,            Object[] paramArrayOfObject) {         this.property_name = paramString;         this.values = paramArrayOfObject;         this.type = paramInt;      }            public Rule setAndOr(int andOr){         this.andOr = andOr;         return this;      }            public int getAndOr(){         return this.andOr;      }      public Object[] getValues() {         return this.values;      }      public int getType() {         return this.type;      }      public String getPropertyName() {         return this.property_name;      }   }}

2.5 Order#

Order类主要用于封装排序规则,代码如下:

package com.gupaoedu.vip.orm.framework;/** * SQL排序组件 */public class Order {   private boolean ascending; //升序还是降序   private String propertyName; //哪个字段升序,哪个字段降序      public String toString() {      return propertyName + ' ' + (ascending ? "asc" : "desc");   }   /**    * Constructor for Order.    */   protected Order(String propertyName, boolean ascending) {      this.propertyName = propertyName;      this.ascending = ascending;   }   /**    * Ascending order    *    * @param propertyName    * @return Order    */   public static Order asc(String propertyName) {      return new Order(propertyName, true);   }   /**    * Descending order    *    * @param propertyName    * @return Order    */   public static Order desc(String propertyName) {      return new Order(propertyName, false);   }}

因篇幅原因,具体的操作类下一篇继续。

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!
如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。

原创不易,坚持很酷,都看到这里了,小伙伴记得点赞、收藏、在看,一键三连加关注!如果你觉得内容太干,可以分享转发给朋友滋润滋润!

免责声明:本网信息来自于互联网,目的在于传递更多信息,并不代表本网赞同其观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,并请自行核实相关内容。本站不承担此类作品侵权行为的直接责任及连带责任。如若本网有任何内容侵犯您的权益,请及时联系我们,本站将会在24小时内处理完毕。
相关文章
返回顶部