一、Mybatis配置
1.1 配置全局文件
在使用MyBtis的时候,我们首先需要配置一个全局配置文件,在这个配置文件中,我们可以配置MyBatis的属性、数据源、插件、别名、基础设置以及SQL配置文件的路径等
其中在<mappers>
标签中,可以定义SQL配置文件的信息,Mybatis提供了四种配置SQL文件的方式,但第四种我们通常都不会用,而第一种和第三种,指定包路径或Mapper接口路径的用法,都需要xml的文件名和接口名一样;而第二种注解指定xml,不需要一样,是因为在xml中的namespace
属性可以指定xml对用的接口是哪一个
基础配置如下:
<configuration>
<!--properties 扫描属性文件.properties -->
<properties resource="db.properties"></properties>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.lizhi.entity.User"/>
</typeAliases>
<plugins>
<plugin interceptor="com.lizhi.plugins.ExamplePlugin" >
</plugin>
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<!--// mybatis内置了JNDI、POOLED、UNPOOLED三种类型的数据源,其中POOLED对应的实现为org.apache.ibatis.datasource.pooled.PooledDataSource,它是mybatis自带实现的一个同步、线程安全的数据库连接池 一般在生产中,我们会使用c3p0或者druid连接池-->
<dataSource type="POOLED">
<property name="driver" value="${mysql.driverClass}"/>
<property name="url" value="${mysql.jdbcUrl}"/>
<property name="username" value="${mysql.user}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--1.必须保证接口名(例如IUserDao)和xml名(IUserDao.xml)相同,还必须在同一个包中-->
<package name="com.lizhi.mapper"/>
<!--2.不用保证同接口同包同名-->
<mapper resource="com/mybatis/mappers/EmployeeMapper.xml"/>
<!--3.保证接口名(例如IUserDao)和xml名(IUserDao.xml)相同,还必须在同一个包中-->
<mapper class="com.mybatis.dao.EmployeeMapper"/>
<!--4.不推荐:引用网路路径或者磁盘路径下的sql映射文件 file:///var/mappers/AuthorMapper.xml-->
<mapper url="file:E:/Study/myeclipse/_03_Test/src/cn/sdut/pojo/PersonMapper.xml"/>
</mappers>
</configuration>
1.2 配置SQL文件
在上面,我们使用的是通过<package>
标签来指定SQL配置,所以SQL配置文件的名称和Mapper接口的名称必须保持一致
SQL配置文件:
<mapper namespace="com.lizhi.mapper.UserMapper">
<cache ></cache>
<!-- Mybatis 是如何将 sql 执行结果封装为目标对象并返回的?都有哪些映射形式?-->
<resultMap id="result" type="com.lizhi.entity.User">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="name" jdbcType="VARCHAR" property="userName"/>
<result column="create_time" jdbcType="DATE" property="createTime"/>
<!--<collection property="" select=""-->
</resultMap>
<select id="selectById" resultMap="result" >
select id,name ,create_time from user
<where>
<if test="id > 0">
and id=#{id}
</if>
</where>
</select>
</mapper>
Mapper接口:
package com.lizhi.mapper;
import com.lizhi.entity.User;
public interface UserMapper {
User selectById(Integer id);
void updateForName(String id,String username);
}
public class User implements Serializable{
private Long id ;
private String userName ;
private Date createTime;
}
1.3 使用Mybatis
上面已经完成了Mybatis的基本配置,下面就可以直接使用了,首先就是去加载配置文件,生成一个SqlSessionFactory,然后再根据SqlSessionFactory创建一个SqlSession,然后通过动态代理的方式获取Mapper接口的代理对象,然后就是调用具体的方法。Mybatis最核心的东西就包括两部分内容:扫描配置类和数据操作,后面会有文件详解介绍Mybatis数据操作的流程,这边文章主要介绍Mybatis是再配置类扫描时,都做了什么,最后生成一个SqlSessionFactory
String resource = "mybatis-config.xml";
//将XML配置文件构建为Configuration配置类
Reader reader = Resources.getResourceAsReader(resource);
// 通过加载配置文件流构建一个SqlSessionFactory 解析xml文件 1
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 数据源 执行器 DefaultSqlSession 2
SqlSession session = sqlSessionFactory.openSession();
try {
// 创建动态代理
UserMapper mapper = session.getMapper(UserMapper.class);
System.out.println(mapper.getClass());
User user = mapper.selectById(1);
System.out.println(user.getUserName());
session.commit();
} catch (Exception e) {
e.printStackTrace();
session.rollback();
} finally {
session.close();
}
二、解析全局配置文件
通过使用Mybatis的代码可以看出,主要是通过SqlSessionFactoryBuilder类的build()方法来实现的,接下来我们详细看下这个build()方法是如何解析的
在Mybatis中,提供了好多种解析类,它们都是继承自BaseBuilder类,在BaseBuilder类中,有一个Configuration属性,它就是用来存放Mybatis所有的
全局配置文件使用XMLConfigBuilder来解析,调用parse()进行解析返回一个Configuration对象,然后再调用build()方法生成一个SqlSessionFactory,我们重点看parse()方法是如何解析的
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
需要注意的是,Mybatis会把xml配置文件解析成一个XNode的类型,这个类型是Mybatis内部定义的,就不具体去看了,调用解析器的evalNode()方法,把<configuration></configuration>
标签解析成一个XNode对象,然后调用parseConfiguration()解析XNode对象的信息
public Configuration parse() {
// 若已经解析过了 就抛出异常
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 设置解析标志位
parsed = true;
// 解析我们的mybatis-config.xml的节点 <configuration></configuration>
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
在parseConfiguration()方法中,就是按顺序来对各个节点进行解析了,下面详细介绍各个节点的解析
private void parseConfiguration(XNode root) {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
/**
* 基本没有用过该属性
* VFS含义是虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。
Mybatis中提供了VFS这个配置,主要是通过该配置可以加载自定义的虚拟文件系统应用程序
解析到:org.apache.ibatis.session.Configuration#vfsImpl
*/
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
}
2.1 属性解析
将<properties/>
标签解析成一个XNode节点,<properties/>
提供了三种配置属性的方式,分别是:
<properties resource="db.properties"></properties>
<properties url="http://www.baidu.com/db.properties"></properties>
<properties>
<property name="lizhi" value="lizhi"/>
</properties>
针对这三种方式的属性配置,Mybatis通过不同的方法来解析,调用getChildrenAsProperties()来解析第三种配置,就是首先获取<properties/>
下的子节点,然后把子节点的name
和value
属性拿出来,作为Properties的key和value;而第一种和第三种方式,都是先获取文件或URL的输入流,然后调用Properties的load()方法来加载
最后把这些属性加入到当前的解析器和configuration属性中
/**
* 解析 properties节点
* <properties resource="mybatis/db.properties" />
* 解析到org.apache.ibatis.parsing.XPathParser#variables
* org.apache.ibatis.session.Configuration#variables
*/
propertiesElement(root.evalNode("properties"));
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
Properties defaults = context.getChildrenAsProperties();
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
2.2 基础设置解析
settings
节点下的配置,都是Mybatis提供的,如果没有配置,Mybatis会使用默认的配置,这一步只是把检验配置是否正确,然后把配置存在settings属性中,后面才会把这些属性设置到Configuration属性当中
/**
* 解析我们的mybatis-config.xml中的settings节点
* 具体可以配置哪些属性:http://www.mybatis.org/mybatis-3/zh/configuration.html#settings
* <settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
..............
</settings>
*
*/
Properties settings = settingsAsProperties(root.evalNode("settings"));
检验配置参数是否正确,localReflectorFactory是ReflectorFactory的一个实例,ReflectorFactory可以根据传入的Class类型,调用findForClass()生成一个Reflector实例,而Reflector实例中包含了该类的类型,可以参数名,可写参数名,以及所有的getter/setter方法
MetaClass对象里面就包含了ReflectorFactory和Reflector两个实例,所以遍历所有的配置,然后判断Configuration类里面是否有这些属性的setter方法,判断逻辑也很简单,就是在创建Reflector实例的时候,会把所有只有一个参数且方法名是以set
开始的方法都拿出来,然后截取set
后面的字符串,同时把首字母变成小写,然后就存在Reflector的setMethods属性中,key就是转换后的方法名,value是把方法封装成了一个MethodInvoker对象,方便通过反射直接调用方法
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
// 其实就是去configuration类里面拿到所有setter方法, 看看有没有当前的配置项
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
}
public class Reflector {
private final Class<?> type;
private final String[] readablePropertyNames;
private final String[] writablePropertyNames;
private final Map<String, Invoker> setMethods = new HashMap<>();
private final Map<String, Invoker> getMethods = new HashMap<>();
private final Map<String, Class<?>> setTypes = new HashMap<>();
private final Map<String, Class<?>> getTypes = new HashMap<>();
private Constructor<?> defaultConstructor;
}
这一步只是对配置项进行了校验,以及缓存属性值,而在后面的方法中,会直接调用configuration的set方法来赋值
// 设置settings 和默认值到configuration
settingsElement(settings);
private void settingsElement(Properties props) {
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
……
}
2.3 日志配置
我们可以通过logImpl
来配置Log的实现类,可以是具体的类,也可以是类的别名。首先就会从别名注册器中直接去拿,如果没有,再通过Class.forName()方法来加载该类,然后设置到configuration中
/**
* 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
* SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
* 解析到org.apache.ibatis.session.Configuration#logImpl
*/
loadCustomLogImpl(settings);
private void loadCustomLogImpl(Properties props) {
Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
configuration.setLogImpl(logImpl);
}
2.4 别名配置
在Configuration类实例化的时候,就回去实例化TypeAliasRegistry(别名处理器)、TypeHandlerRegistry(类型处理器)、MapperRegistry(mapper接口注册器)等等这些属性,而这些属性在实例化的时候都有一些默认的值,以别名处理器TypeAliasRegistry为例,就会添加默认的一些别名:
/**
* mybaits对我们默认的别名支撑
*/
public TypeAliasRegistry() {
registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
……
}
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// issue #748
String key = alias.toLowerCase(Locale.ENGLISH);
if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
}
typeAliases.put(key, value);
}
但是在使用Mybatis的时候,也可以通过<typeAliases></typeAliases>
节点来配置别名
该节点提供了两种配置别名的方式,如果是通过<package>
来指定某个类包,则会去遍历包下面所有类,除了接口、匿名类、内部类等,其他都会去加载,然后缓存在别名解析器中
如果是通过alias
和type
配置别名,就直接加载type
指定的类
对于这两种配置方式,如果没有指定别名的名称,Mybatis首先会去看类上是否有@Alias注解,把它的value()值最为别名,否则就调用Class类的getSimpleName()方法,返回值作为别名
最后就是把别名和对应的类注册到configuration属性的TypeAliasRegistry属性中即可
/**
* 解析我们的别名
* <typeAliases>
<typeAlias alias="Author" type="cn.tulingxueyuan.pojo.Author"/>
</typeAliases>
<typeAliases>
<package name="cn.tulingxueyuan.pojo"/>
</typeAliases>
解析到oorg.apache.ibatis.session.Configuration#typeAliasRegistry.typeAliases
除了自定义的,还有内置的
*/
typeAliasesElement(root.evalNode("typeAliases"));
// 注册别名
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
}
}
}
}
2.5 插件配置
Mybatis中提供了四大类型的插件,分别作用于Executor、ParameterHandler、StatementHandler和ResultSetHandler,Executor是一个执行器,SqlSession内部真正工作的就是一个Executor对象
遍历plugins
节点下所有子节点,获取这些子节点的属性值,创建一个拦截器的实例,然后把属性添加到实例中,最后把这些拦截器添加到configuration属性的拦截器链中
/**
* 解析我们的插件(比如分页插件)
* mybatis自带的
* Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
解析到:org.apache.ibatis.session.Configuration#interceptorChain.interceptors
*/
pluginElement(root.evalNode("plugins"));
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
我们通过实现Interceptor接口来定义一个拦截器,但在类上面,需要使用@Intercepts注解来表明该拦截器的类型,以及需要拦截的方法
@Intercepts({@Signature( type= Executor.class, method = "query", args ={
MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
})})
public class ExamplePlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("代理");
Object[] args = invocation.getArgs();
MappedStatement ms= (MappedStatement) args[0];
// 执行下一个拦截器、直到尽头
return invocation.proceed();
}
}
2.6 数据源环境配置
在配置文件中,可以配置多个数据源,但只有一个会生效,也就是environments
的default
指定的一个才会生效
/**
* 解析我们的mybatis环境
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="root"/>
<property name="password" value="Zw726515"/>
</dataSource>
</environment>
</environments>
* 解析到:org.apache.ibatis.session.Configuration#environment
* 在集成spring情况下由 spring-mybatis提供数据源 和事务工厂
*/
environmentsElement(root.evalNode("environments"));
通过transactionManagerElement()方法来解析transactionManager
节点,生成一个事务工厂,用于创建事务
然后通过dataSourceElement()方法解析dataSource
节点,生成一个数据库连接工厂,然后调用getDataSource()方法获得数据库连接信息,最后把这些信息,封装成一个Environment实例,设置到configuration中
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
在解析数据库事务类型和连接池类型时,都会用别名去获取具体的类,以事务为例:
我们通常使用的时候,直接指定别名就行了,获得指定的type
类型后,会调用resolveClass()方法来解析对应的Class,其实在Configuration类实例化的时候,可会注册一些别名
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type"); // JDBC
Properties props = context.getChildrenAsProperties();
TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a TransactionFactory.");
}
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
……
}
2.7 数据库厂商配置
解析所有databaseIdProvider
节点的属性,然后通过上面配置的数据源,创建一个数据库连接来获取数据库产品的名称(使用完就关掉),然后于节点的name属性匹配,将value值设置到configuration中
/**
* 解析数据库厂商
* <databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
<property name="MySql" value="mysql" />
</databaseIdProvider>
* 解析到:org.apache.ibatis.session.Configuration#databaseId
*/
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
private void databaseIdProviderElement(XNode context) throws Exception {
DatabaseIdProvider databaseIdProvider = null;
if (context != null) {
String type = context.getStringAttribute("type");
// awful patch to keep backward compatibility
if ("VENDOR".equals(type)) {
type = "DB_VENDOR";
}
Properties properties = context.getChildrenAsProperties();
databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
databaseIdProvider.setProperties(properties);
}
Environment environment = configuration.getEnvironment();
if (environment != null && databaseIdProvider != null) {
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
configuration.setDatabaseId(databaseId);
}
}
2.8 类型处理器配置
类型处理器的用途就很多了,尤其是在处理查询的结果集的时候,就需要用到类型处理器,而Mybatis也给我们提供了很多内置的类型处理器,都在Configuration的TypeHandlerRegistry实例中,在TypeHandlerRegistry实例化的时候,会添加默认的类型处理器
public TypeHandlerRegistry() {
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
……
register(String.class, JdbcType.CHAR, new StringTypeHandler());
register(String.class, JdbcType.CLOB, new ClobTypeHandler());
register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
……
}
自定义类型转换器需要实现TypeHandler接口,并通过@MappedJdbcTypes和@MappedTypes两个注解来指定相互转换的类型
@MappedTypes(JSONObject.class)
@MappedJdbcTypes(JdbcType.JSON)
public class JsonTypeHandler implements TypeHandler {
……
}
一般情况下,这些默认的类型处理器就够用了,但我们也可以自定义类型处理器,类型转换器的解析于别名配置的解析基本一样
/**
* 解析我们的类型处理器节点
* <typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
解析到:org.apache.ibatis.session.Configuration#typeHandlerRegistry.typeHandlerMap
*/
typeHandlerElement(root.evalNode("typeHandlers"));
private void typeHandlerElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
Class<?> javaTypeClass = resolveClass(javaTypeName);
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/112140.html