1 JDBC操作数据库问题分析
JDBC API 允许应用程序访问任何形式的表格数据,特别是存储在关系数据库中的数据
代码示例:
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 通过驱动管理类获取数据库链接
connection =
DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis? characterEncoding=utf-8", "root", "root");
// 定义sql语句?表示占位符
String sql = "select * from user where username = ?";
// 获取预处理statement
preparedStatement = connection.prepareStatement(sql);
// 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值 preparedStatement.setString(1, "tom");
// 向数据库发出sql执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
// 遍历查询结果集
while (resultSet.next()) {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
// 封装User
user.setId(id);
user.setUsername(username);
}
System.out.println(user);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
2 JDBC问题分析和解决思路
剖开代码,逐个分析:
(1)加载驱动,获取链接:
-
存在问题1:数据库配置信息存在硬编码问题。
优化思路:使用配置文件!
-
存在问题2:频繁创建、释放数据库连接问题。
优化思路:使用数据连接池!
(2)定义sql、设置参数、执行查询:
-
存在问题3:SQL语句、设置参数、获取结果集参数均存在硬编码问题 。
优化思路:使用配置文件!
(2)遍历查询结果集:
-
存在问题4:手动封装返回结果集,较为繁琐
优化思路:使用Java反射、内省!
针对JDBC各个环节中存在的不足,现在,我们整理出对应的优化思路,统一汇总:
存在问题 | 优化思路 |
---|---|
数据库配置信息存在硬编码问题 | 使用配置文件 |
频繁创建、释放数据库连接问题 | 使用数据连接池 |
SQL语句、设置参数、获取结果集参数均存在硬编码问题 | 使用配置文件 |
手动封装返回结果集,较为繁琐 | 使用Java反射、内省 |
3 自定义持久层框架_思路分析
JDBC是个人作战,凡事亲力亲为,低效而高险,自己加载驱动,自己建连接,自己 …
而持久层框架好比是多工种协作,分工明确,执行高效,有专门负责解析注册驱动建立连接的,有专门管理数据连接池的,有专门执行sql语句的,有专门做预处理参数的,有专门装配结果集的 …
优化思路: 框架的作用,就是为了帮助我们减去繁重开发细节与冗余代码,使我们能更加专注于业务应用开发。
3.1 使用JDBC和使用持久层框架区别
是不是发现,拥有这么一套持久层框架是如此舒适,我们仅仅需要干两件事:
- 配置数据源(地址/数据名/用户名/密码)
- 编写SQL与参数准备(SQL语句/参数类型/返回值类型)
框架,除了思考本身的工程设计,还需要考虑到实际项目端的使用场景,干系方涉及两端:
- 使用端(实际项目)
- 持久层框架本身
以上两步,我们通过一张架构图《 手写持久层框架基本思路 》来梳理清楚:
3.2 核心接口/类重点说明
分工协作 | 角色定位 | 类名定义 |
---|---|---|
负责读取配置文件 | 资源辅助类 | Resources |
负责存储数据库连接信息 | 数据库资源类 | Configuration |
负责存储SQL映射定义、存储结果集映射定义 | SQL与结果集资源类 | MappedStatement |
负责解析配置文件,创建会话工厂SqlSessionFactory | 会话工厂构建者 | SqlSessionFactoryBuilder |
负责创建会话SqlSession | 会话工厂 | SqlSessionFactory |
指派执行器Executor | 会话 | SqlSession |
负责执行SQL (配合指定资源Mapped Statement) | 执行器 | Executor |
正常来说项目只对应一套数据库环境,一般对应一个SqlSessionFactory实例对象,我们使用单例模式只创建一个SqlSessionFactory实例。
如果需要配置多套数据库环境,那需要做一些拓展,例如Mybatis中通过environments等配置就可以支持多套测试/生产数据库环境进行切换。
3.3 项目使用端
(1)调用框架API,除了引入自定义持久层框架的jar包
(2)提供两部分配置信息:1.sqlMapConfig.xml : 数据库配置信息(地址/数据名/用户名/密码),以及mapper.xml的全路径
2.mapper.xml : SQL配置信息,存放SQL语句、参数类型、返回值类型相关信息
3.4 自定义框架本身
1、加载配置文件:根据配置文件的路径,加载配置文件成字节输入流,存储在内存中。
2、 创建两个javaBean(容器对象):存放配置文件解析出来的内容
3、解析配置文件(使用dom4j) ,并创建SqlSession会话对象
4、创建SqlSessionFactory接口以及实现类DefaultSqlSessionFactory
5、创建SqlSession接口以及实现类DefaultSqlSession
6、创建Executor接口以及实现类SimpleExecutor
基本过程我们已经清晰,我们再细化一下类图,更好的助于我们实际编码:
3.5 最终手写的持久层框架结构参考
4 自定义持久层框架_编码
<properties>
<!-- Encoding -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<!--引入ipersistent的依赖-->
在使用端项目中创建配置配置文件
创建 sqlMapConfig.xml
<configuration>
<!--1.配置数据库配置信息-->
<dataSource>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql:///zdy_mybatis?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</dataSource>
<!--2.引入映射配置文件-->
<mappers>
<mapper resource="mapper/UserMapper.xml"></mapper>
</mappers>
</configuration>
mapper.xml
<mapper namespace="User">
<!--根据条件查询单个-->
<select id="selectOne" resultType="com.oldlu.pojo.User" parameterType="com.oldlu.pojo.User">
select * from user where id = #{id} and username = #{username}
</select>
<!--查询所有-->
<select id="selectList" resultType="com.oldlu.pojo.User">
select * from user
</select>
</mapper>
User实体
public class User {
//主键标识
private Integer id;
//用户名
private String username;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' + '}';
}
}
再创建一个Maven子工程并且导入需要用到的依赖坐标
<properties>
<!-- Encoding -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!-- mysql 依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<!--junit 依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<!--作用域测试范围-->
<scope>test</scope>
</dependency>
<!--dom4j 依赖-->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!--xpath 依赖-->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
<!--druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<!-- log日志 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
Resources
public class Resources {
/**
* 根据配置文件的路径,加载成字节输入流,存到内存中
* @param path
* @return
*/
public static InputStream getResourceAsSteam(String path){
InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
return resourceAsStream;
}
Configuration
/**
* 存放核心配置文件解析的内容
*/
public class Configuration {
// 数据源对象
private DataSource dataSource;
// map : key :statementId value : 封装好的MappedStatement
private Map<String,MappedStatement> mappedStatementMap = new HashMap<>();
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public Map<String, MappedStatement> getMappedStatementMap() {
return mappedStatementMap;
}
public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) {
this.mappedStatementMap = mappedStatementMap;
}
}
MappedStatement
/**
* 存放解析映射配置文件的内容
* <select id="selectOne" resultType="com.oldlu.pojo.User" parameterType="com.oldlu.pojo.User">
* select * from user where id = #{id} and username = #{username}
* </select>
*/
public class MappedStatement {
// 1.唯一标识 (statementId namespace.id)
private String statementId;
// 2.返回结果类型
private String resultType;
// 3.参数类型
private String parameterType;
// 4.要执行的sql语句
private String sql;
// 5.mapper代理:sqlcommandType
private String sqlcommandType;
public String getSqlcommandType() {
return sqlcommandType;
}
public void setSqlcommandType(String sqlcommandType) {
this.sqlcommandType = sqlcommandType;
}
public String getStatementId() {
return statementId;
}
public void setStatementId(String statementId) {
this.statementId = statementId;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
public String getParameterType() {
return parameterType;
}
public void setParameterType(String parameterType) {
this.parameterType = parameterType;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
}
SqlSessionFactoryBuilder
public class SqlSessionFactoryBuilder {
/**
* 1.解析配置文件,封装Configuration 2.创建SqlSessionFactory工厂对象
* @return
*/
public SqlSessionFactory build(InputStream inputStream) throws DocumentException {
// 1.解析配置文件,封装Configuration
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
Configuration configuration = xmlConfigBuilder.parse(inputStream);
SqlSessionFactory defatultSqlSessionFactory = new DefatultSqlSessionFactory(configuration);
return defatultSqlSessionFactory;
}
}
XMLConfigerBuilder
public class XMLConfigBuilder {
private Configuration configuration;
public XMLConfigBuilder() {
configuration = new Configuration();
}
/**
* 使用dom4j解析xml文件,封装configuration对象
* @param inputStream
* @return
*/
public Configuration parse(InputStream inputStream) throws DocumentException {
Document document = new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
// 解析核心配置文件中数据源部分
List<Element> list = rootElement.selectNodes("//property");
// <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
Properties properties = new Properties();
for (Element element : list) {
String name = element.attributeValue("name");
String value = element.attributeValue("value");
properties.setProperty(name,value);
}
// 创建数据源对象(连接池)
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(properties.getProperty("driverClassName"));
druidDataSource.setUrl(properties.getProperty("url"));
druidDataSource.setUsername(properties.getProperty("username"));
druidDataSource.setPassword(properties.getProperty("password"));
// 创建好的数据源对象封装进configuration中、
configuration.setDataSource(druidDataSource);
// 解析映射配置文件
// 1.获取映射配置文件的路径 2.解析 3.封装好mappedStatement
List<Element> mapperList = rootElement.selectNodes("//mapper");
for (Element element : mapperList) {
String mapperPath = element.attributeValue("resource");
InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath);
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
xmlMapperBuilder.parse(resourceAsSteam);
}
return configuration;
}
}
XMLMapperBuilder
public class XMLMapperBuilder {
private Configuration configuration;
public XMLMapperBuilder(Configuration configuration) {
this.configuration = configuration;
}
public void parse(InputStream inputStream) throws DocumentException, ClassNotFoundException {
Document document = new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
String namespace = rootElement.attributeValue("namespace");
List<Element> select = rootElement.selectNodes("select");
for (Element element : select) { //id的值
String id = element.attributeValue("id");
String paramterType = element.attributeValue("paramterType");
String resultType = element.attributeValue("resultType"); //输入参数class
Class<?> paramterTypeClass = getClassType(paramterType);
//返回结果class
Class<?> resultTypeClass = getClassType(resultType);
//statementId
String key = namespace + "." + id;
//sql语句
String textTrim = element.getTextTrim();
//封装 mappedStatement
MappedStatement mappedStatement = new MappedStatement();
mappedStatement.setId(id);
mappedStatement.setParamterType(paramterTypeClass);
mappedStatement.setResultType(resultTypeClass);
mappedStatement.setSql(textTrim);
//填充 configuration
configuration.getMappedStatementMap().put(key, mappedStatement);
private Class<?> getClassType (String paramterType) throws ClassNotFoundException {
Class<?> aClass = Class.forName(paramterType);
return aClass;
}
}
sqlSessionFactory 接口及D efaultSqlSessionFactory 实现类
public interface SqlSessionFactory {
/**
* 生产sqlSession :封装着与数据库交互的方法
* @return
*/
public SqlSession openSession();
}
public class DefatultSqlSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefatultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
// 执行器创建出来
Executor executor = new SimpleExecutor();
DefaultSqlSession defaultSqlSession = new DefaultSqlSession(configuration,executor);
return defaultSqlSession;
}
}
sqlSession 接口及 DefaultSqlSession 实现类
public interface SqlSession {
/**
* 查询所有的方法 select * from user where username like '%aaa%' and sex = ''
* 参数1:唯一标识
* 参数2:入参
*/
public <E> List<E> selectList(String statementId,Object parameter) throws Exception;
/**
* 查询单个的方法
*/
public <T> T selectOne(String statementId,Object parameter) throws Exception;
}
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration, Executor executor) {
this.configuration = configuration;
this.executor = executor;
}
@Override // user.selectList 1 tom user
public <E> List<E> selectList(String statementId, Object params) throws Exception {
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
// 将查询操作委派给底层的执行器
List<E> list = executor.query(configuration,mappedStatement,params);
return list;
}
@Override
public <T> T selectOne(String statementId, Object params) throws Exception {
List<Object> list = this.selectList(statementId, params);
if(list.size() == 1){
return (T) list.get(0);
}else if(list.size() > 1){
throw new RuntimeException("返回结果过多");
}else {
return null;
}
}
}
Executor
public interface Executor {
<E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object params) throws Exception;
}
SimpleExecutor
public class SimpleExecutor implements Executor {
/**
* 执行JDBC操作
* @param configuration
* @param mappedStatement
* @param params
* @param <E>
* @return
*/
@Override // user product
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object params) throws Exception {
// 1. 加载驱动,获取连接
Connection connection = configuration.getDataSource().getConnection();
// 2. 获取prepareStatement预编译对象
/*
select * from user where id = #{id} and username = #{username}
select * from user where id = ? and username = ?
占位符替换 :#{}替换成? 注意:#{id}里面的id名称要保存
*/
String sql = mappedStatement.getSql();
BoundSql boundSql = getBoundSQL(sql);
String finaLSql = boundSql.getFinaLSql();
PreparedStatement preparedStatement = connection.prepareStatement(finaLSql);
// 3.设置参数
// 问题1: Object param(类型不确定 user/product/map/String)
// 问题2:该把对象中的哪一个属性赋值给哪一个占位符呢?
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if(parameterMappings.size() > 0){
// com.oldlu.pojo.User
String parameterType = mappedStatement.getParameterType();
Class<?> parameterTypeClass = Class.forName(parameterType);
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
// id
String content = parameterMapping.getContent();
// 反射
Field declaredField = parameterTypeClass.getDeclaredField(content);
// 暴力访问
declaredField.setAccessible(true);
Object value = declaredField.get(params);
preparedStatement.setObject(i+1 ,value);
}
}
// 4.执行sql,发起查询
ResultSet resultSet = preparedStatement.executeQuery();
String resultType = mappedStatement.getResultType();
Class<?> resultTypeClass = Class.forName(resultType);
ArrayList<E> list = new ArrayList<>();
// 5.遍历封装
while (resultSet.next()){
// 元数据信息中包含了字段名 字段的值
ResultSetMetaData metaData = resultSet.getMetaData();
Object obj = resultTypeClass.newInstance();
for (int i = 1; i <= metaData.getColumnCount() ; i++) {
// id username
String columnName = metaData.getColumnName(i);
Object value = resultSet.getObject(columnName);
// 属性描述器
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultTypeClass);
Method writeMethod = propertyDescriptor.getWriteMethod();
writeMethod.invoke(obj,value);
}
list.add((E) obj);
}
return list;
}
/**
* 1.将sql中#{}替换成? 2.将#{}里面的值保存
* @param sql
* @return
*/
private BoundSql getBoundSQL(String sql) {
// 标记处理器:配合标记解析器完成标记的解析工作
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
// 标记解析器
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
String finalSql = genericTokenParser.parse(sql);
// #{}里面的值的集合
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
BoundSql boundSql = new BoundSql(finalSql, parameterMappings);
return boundSql;
}
}
BoundSql
public class BoundSql {
//解析过后的sql语句
private String sqlText;
//解析出来的参数
private List<ParameterMapping> parameterMappingList = new ArrayList<ParameterMapping>();
public BoundSql(String sqlText, List<ParameterMapping>
parameterMappingList) {
this.sqlText = sqlText;
this.parameterMappingList = parameterMappingList;
}
public String getSqlText() {
return sqlText;
}
public void setSqlText(String sqlText) {
this.sqlText = sqlText;
}
public List<ParameterMapping> getParameterMappingList() {
return parameterMappingList;
}
public void setParameterMappingList(List<ParameterMapping> parameterMappingList) {
this.parameterMappingList = parameterMappingList;
}
}
5 自定义持久层框架优化
通过上述我们的自定义框架,我们解决了JDBC操作数据库带来的一些问题:例如频繁创建释放数据库连 接,硬编码,手动封装返回结果集等问题,但是现在我们继续来分析刚刚完成的自定义框架代码,有没 有什么问题?
问题如下:
- dao的实现类中存在重复的代码,整个操作的过程模板重复(创建sqlsession,调用sqlsession方 法,关闭 sqlsession)
- dao的实现类中存在硬编码,调用sqlsession的方法时,参数statement的id硬编码
解决:使用代理模式来创建接口的代理对象
@Test
public void test2() throws Exception {
InputStream resourceAsSteam = Resources.getResourceAsSteam(path: "sqlMapConfig.xml")
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsSteam);
SqlSession sqlSession = build.openSession();
User user = new User();
user.setld(l);
user.setUsername("tom");
//代理对象
UserMapper userMapper = sqlSession.getMappper(UserMapper.class);
User userl = userMapper.selectOne(user);
System・out.println(userl);
}
在sqlSession中添加方法文章来源:https://www.toymoban.com/news/detail-705074.html
public interface SqlSession {
public <T> T getMappper(Class<?> mapperClass);
实现类文章来源地址https://www.toymoban.com/news/detail-705074.html
package com.oldlu.sqlSession;
import com.oldlu.executor.Executor;
import com.oldlu.pojo.Configuration;
import com.oldlu.pojo.MappedStatement;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.List;
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration, Executor executor) {
this.configuration = configuration;
this.executor = executor;
}
@Override
public <E> List<E> selectList(String statementId, Object param) throws Exception {
// 要传递什么参数呢?
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
List<E> list = executor.query(configuration,mappedStatement,param);
return list;
}
@Override
public <T> T selectOne(String statementId, Object param) throws Exception {
// 调用selectList方法
List<Object> list = selectList(statementId, param);
if(list.size() == 1){
return (T) list.get(0);
}else if(list.size() > 1){
throw new RuntimeException("返回结果过多...");
}
return null;
}
/**
* 生成代理对象
* @param mapperClass
* @param <T>
* @return
*/
@Override
public <T> T getMapper(Class<?> mapperClass) {
// 使用JDK动态代理生成代理对象
Object proxyInstance = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
// 参数1:Object o:代理对象的引用,很少用
// 参数2:Method method :当前被调用的方法对象
// 参数3:Object[] objects:被调用的方法的参数
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
// 具体的逻辑:执行底层的JDBC
// 思路:通过调用sqlSession的方法来完成执行
// 问题1:如何获取statementId 根据method获取
Class<?> declaringClass = method.getDeclaringClass();
// 类全路径= namespace的值
String className = declaringClass.getName();
String methodName = method.getName();
String statementId = className + "." + methodName;
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
// 问题2:该调用增删改查什么方法呢? 优化:sqlCommandType
String sqlCommandType = mappedStatement.getSqlCommandType();
switch (sqlCommandType){
case "select":
//查询操作
//问题3:调selectOne还是调selectAll呢?
Class<?> returnType = method.getReturnType();
boolean assignableFrom = Collection.class.isAssignableFrom(returnType);
if(assignableFrom){
if(mappedStatement.getParameterType() !=null) {
return selectList(statementId, objects[0]);
}
return selectList(statementId, null);
}
return selectOne(statementId,objects[0]);
case "update":
// 更新操作
break;
case "insert":
// 更新操作
break;
case "delete":
// 更新操作
break;
}
return null;
}
});
return (T) proxyInstance;
}
}
到了这里,关于MyBatis原理分析手写持久层框架的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!