mybatis+spring+springmvc(传智播客)笔记

更新时间:2024-06-14 03:23:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

javaEE框架课程

Mybatis

框架课程

javaEE框架课程

1 Mybatis入门

1.1 单独使用jdbc编程问题总结 1.1.1 jdbc程序

Publicstaticvoid main(String[] args) { Connection connection = null;

PreparedStatement preparedStatement = null; ResultSet resultSet = null;

try {

//加载数据库驱动

Class.forName(\);

//通过驱动管理类获取数据库链接 connection =

DriverManager.getConnection(\terEncoding=utf-8\, \, \);

//定义sql语句 ?表示占位符

String sql = \; //获取预处理statement

preparedStatement = connection.prepareStatement(sql);

//设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值

preparedStatement.setString(1, \王五\); //向数据库发出sql执行查询,查询出结果集

resultSet = preparedStatement.executeQuery(); //遍历查询结果集

while(resultSet.next()){

System.out.println(resultSet.getString(\)+\\+resultSet.getString(\)); }

} catch (Exception e) { e.printStackTrace(); }finally{

//释放资源

if(resultSet!=null){ try {

resultSet.close();

} catch (SQLException e) {

javaEE框架课程

}

}

// TODO Auto-generated catch block e.printStackTrace(); } }

if(preparedStatement!=null){ try {

preparedStatement.close(); } catch (SQLException e) {

// TODO Auto-generated catch block e.printStackTrace(); } }

if(connection!=null){ try {

connection.close(); } catch (SQLException e) {

// TODO Auto-generated catch block e.printStackTrace(); } }

上边使用jdbc的原始方法(未经封装)实现了查询数据库表记录的操作。

1.1.2 jdbc编程步骤:

1、 加载数据库驱动

2、 创建并获取数据库链接 3、 创建jdbc statement对象 4、 设置sql语句

5、 设置sql语句中的参数(使用preparedStatement) 6、 通过statement执行sql并获取结果 7、 对sql执行结果进行解析处理

8、 释放资源(resultSet、preparedstatement、connection)

1.1.3 jdbc问题总结如下:

1、 数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接

池可解决此问题。

javaEE框架课程

2、 Sql语句在代码中硬编码,造成代码不易维护,实际应用sql变化的可能较大,sql变动

需要改变java代码。

3、 使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不

一定,可能多也可能少,修改sql还要修改代码,系统不易维护。 4、 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如

果能将数据库记录封装成pojo对象解析比较方便。

1.2 MyBatis介绍

MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis,实质上Mybatis对ibatis进行一些改进。

MyBatis是一个优秀的持久层框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建connection、创建statement、手动设置参数、结果集检索等jdbc繁杂的过程代码。

Mybatis通过xml或注解的方式将要执行的各种statement(statement、

preparedStatemnt、CallableStatement)配置起来,并通过java对象和statement中的sql进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射成java对象并返回。

1.3 Mybatis架构

javaEE框架课程

1、 mybatis配置 SqlMapConfig.xml,此文件作为mybatis的全局配置文件,配置了mybatis的运行环境等信息。 mapper.xml文件即sql映射文件,文件中配置了操作数据库的sql语句。此文件需要在SqlMapConfig.xml中加载。

2、 通过mybatis环境等配置信息构造SqlSessionFactory即会话工厂

3、 由会话工厂创建sqlSession即会话,操作数据库需要通过sqlSession进行。 4、 mybatis底层自定义了Executor执行器接口操作数据库,Executor接口有两个实现,一个

是基本执行器、一个是缓存执行器。

5、 Mapped Statement也是mybatis一个底层封装对象,它包装了mybatis配置信息及sql

映射信息等。mapper.xml文件中一个sql对应一个Mapped Statement对象,sql的id即是Mapped statement的id。

6、 Mapped Statement对sql执行输入参数进行定义,包括HashMap、基本类型、pojo,Executor

通过Mapped Statement在执行sql前将输入的java对象映射至sql中,输入参数映射就是jdbc编程中对preparedStatement设置参数。 7、 Mapped Statement对sql执行输出结果进行定义,包括HashMap、基本类型、pojo,Executor

通过Mapped Statement在执行sql后将输出结果映射至java对象中,输出结果映射过程相当于jdbc编程中对结果的解析处理过程。

1.4 mybatis下载

mybaits的代码由github.com管理,地址:https://github.com/mybatis/mybatis-3/releases

mybatis-3.2.7.jar----mybatis的核心包 lib----mybatis的依赖包

mybatis-3.2.7.pdf----mybatis使用手册

1.5 创建mysql数据库

先导入sql_table.sql,再导入sql_data.sql脚本:

javaEE框架课程

List list =

sqlSession.selectList(\, \张\); System.out.println(list.size()); } catch (Exception e) { e.printStackTrace(); } finally {

if (sqlSession != null) { sqlSession.close(); } }

}

}

1.6.7.1.4 #{}和${}

#{}表示一个占位符号,通过#{}可以实现preparedStatement向占位符中设置值,自动进行java类型和jdbc类型转换,#{}可以有效防止sql注入。#{}可以接收简单类型值或pojo属性值。如果parameterType传输单个简单类型值,#{}括号中可以是value或其它名称。

${}表示拼接sql串,通过${}可以将parameterType 传入的内容拼接在sql中且不进行jdbc类型转换,${}可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,${}括号中只能是value。

1.6.7.1.5 parameterType和resultType

parameterType:指定输入参数类型,mybatis通过ognl从输入对象中获取参数值拼接在sql中。

resultType:指定输出结果类型,mybatis将sql查询结果的一行记录数据映射为resultType指定类型的对象。

1.6.7.1.6 selectOne和selectList

selectOne查询一条记录,如果使用selectOne查询多条记录则抛出异常:

org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 3 at

javaEE框架课程

org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:70)

selectList可以查询一条或多条记录。

1.6.7.2 添加

1.6.7.2.1 映射文件:

在SqlMapConfig.xml中添加:

select LAST_INSERT_ID()

insert into user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address})

1.6.7.2.2 测试程序:

// 添加用户信息 @Test

publicvoid testInsert() {

// 数据库会话实例

SqlSession sqlSession = null; try {

// 创建数据库会话实例sqlSession

sqlSession = sqlSessionFactory.openSession(); // 添加用户信息

Useruser =new User();

user.setUsername(\张小明\); user.setAddress(\河南郑州\); user.setSex(\);

user.setPrice(1999.9f);

sqlSession.insert(\, user);

//提交事务

sqlSession.commit(); } catch (Exception e) {

javaEE框架课程

}

e.printStackTrace(); } finally {

if (sqlSession != null) { sqlSession.close(); } }

1.6.7.2.3 mysql自增主键返回

通过修改sql映射文件,可以将mysql自增主键返回:

\>

insert into user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address});

添加selectKey实现将主键返回

keyProperty:返回的主键存储在pojo中的哪个属性

order:selectKey的执行顺序,是相对与insert语句来说,由于mysql的自增原理执行完insert语句之后才将主键生成,所以这里selectKey的执行顺序为after resultType:返回的主键是什么类型

LAST_INSERT_ID():是mysql的函数,返回auto_increment自增列新记录id值。

1.6.7.2.4 Mysql使用uuid实现主键

需要增加通过select uuid()得到uuid值

insert into user(id,username,birthday,sex,address) values(#{id},#{username},#{birthday},#{sex},#{address})

注意这里使用的order是“BEFORE”

javaEE框架课程

1.6.7.2.5 Oracle使用序列生成主键

首先自定义一个序列且用于生成主键,selectKey使用如下:

SELECT 自定义序列.NEXTVAL FROM DUAL

insert into user(id,username,birthday,sex,address) values(#{id},#{username},#{birthday},#{sex},#{address})

注意这里使用的order是“BEFORE”

1.6.7.3 删除

1.6.7.3.1 映射文件:

delete from userwhere id=#{id}

1.6.7.3.2 测试程序:

// 根据id删除用户 @Test

publicvoidtestDelete() {

// 数据库会话实例

SqlSession sqlSession = null; try {

// 创建数据库会话实例sqlSession

sqlSession = sqlSessionFactory.openSession(); // 删除用户

sqlSession.delete(\,18);

javaEE框架课程

}

// 提交事务

sqlSession.commit(); } catch (Exception e) { e.printStackTrace(); } finally {

if (sqlSession != null) { sqlSession.close(); } }

1.6.7.4 修改 1.6.7.4.1 映射文件

update userset

username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id=#{id}

1.6.7.4.2 测试程序

// 更新用户信息 @Test

publicvoidtestUpdate() {

// 数据库会话实例

SqlSession sqlSession = null; try {

// 创建数据库会话实例sqlSession

sqlSession = sqlSessionFactory.openSession(); // 添加用户信息

Useruser =new User(); user.setId(16);

user.setUsername(\张小明\); user.setAddress(\河南郑州\); user.setSex(\);

user.setPrice(1999.9f);

javaEE框架课程

}

sqlSession.update(\, user); // 提交事务

sqlSession.commit();

} catch (Exception e) { e.printStackTrace(); } finally {

if (sqlSession != null) { sqlSession.close(); } }

1.6.8 Mybatis解决jdbc编程的问题

1、 数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接

池可解决此问题。

解决:在SqlMapConfig.xml中配置数据链接池,使用连接池管理数据库链接。

2、 Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改

变java代码。

解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。

3、 向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符

需要和参数一一对应。

解决:Mybatis自动将java对象映射至sql语句,通过statement中的parameterType定义输入参数的类型。

4、 对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库

记录封装成pojo对象解析比较方便。

解决:Mybatis自动将sql执行结果映射至java对象,通过statement中的resultType定义输出结果的类型。

1.6.9 与hibernate不同

Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句,不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句,并将java对象和sql语句映射生成最终执行的sql,最后将sql执行的结果再映射生成java对象。

Mybatis学习门槛低,简单易学,程序员直接编写原生态sql,可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁,一但需求变化要求成果输出迅速。但是灵活的前提是

javaEE框架课程

mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。

Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件(例如需求固定的定制化软件)如果用hibernate开发可以节省很多代码,提高效率。但是Hibernate的学习门槛高,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如何权衡,以及怎样用好Hibernate需要具有很强的经验和能力才行。

总之,按照用户的需求在有限的资源环境下只要能做出维护性、扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。

2 Dao开发方法

使用Mybatis开发Dao,通常有两个方法,即原始Dao开发方法和Mapper接口开发方法。

2.1 需求

将下边的功能实现Dao:

根据用户id查询一个用户信息

根据用户名称模糊查询用户信息列表 添加用户信息

2.2 SqlSession的使用范围

SqlSession中封装了对数据库的操作,如:查询、插入、更新、删除等。

通过SqlSessionFactory创建SqlSession,而SqlSessionFactory是通过SqlSessionFactoryBuilder进行创建。

2.2.1 SqlSessionFactoryBuilder

SqlSessionFactoryBuilder用于创建SqlSessionFacoty,SqlSessionFacoty一旦创建完成就不需要SqlSessionFactoryBuilder了,因为SqlSession是通过SqlSessionFactory生产,所以可以将SqlSessionFactoryBuilder当成一个工具类使用,最佳使用范围是方法范围即方法体内局部变量。

javaEE框架课程

2.2.2 SqlSessionFactory

SqlSessionFactory是一个接口,接口中定义了openSession的不同重载方法,SqlSessionFactory的最佳使用范围是整个应用运行期间,一旦创建后可以重复使用,通常以单例模式管理SqlSessionFactory。

2.2.3 SqlSession

SqlSession是一个面向用户的接口,sqlSession中定义了数据库操作,默认使用DefaultSqlSession实现类。

执行过程如下:

1、 加载数据源等配置信息

Environment environment = configuration.getEnvironment();

2、 创建数据库链接 3、 创建事务对象

4、 创建Executor,SqlSession所有操作都是通过Executor完成,mybatis源码如下:

if (ExecutorType.BATCH == executorType) {

executor = newBatchExecutor(this, transaction); } elseif (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else {

executor = new SimpleExecutor(this, transaction); }

if (cacheEnabled) {

executor = new CachingExecutor(executor, autoCommit); }

5、 SqlSession的实现类即DefaultSqlSession,此对象中对操作数据库实质上用的是Executor

结论: 每个线程都应该有它自己的SqlSession实例。SqlSession的实例不能共享使用,它也是线程不安全的。因此最佳的范围是请求或方法范围。绝对不能将SqlSession实例的引用放在一个类的静态字段或实例字段中。 打开一个 SqlSession;使用完毕就要关闭它。通常把这个关闭操作放到 finally 块中以确保每次都能执行关闭。如下: SqlSession session = sqlSessionFactory.openSession(); try { // do work } finally { session.close();

javaEE框架课程

}

2.3 原始Dao开发方式

原始Dao开发方法需要程序员编写Dao接口和Dao实现类。

2.3.1 映射文件

PUBLIC\

\>

select * from userwhere id = #{id}

select LAST_INSERT_ID()

insert into user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address})

2.3.2 Dao接口

Publicinterface UserDao {

publicUser getUserById(int id) throws Exception; publicvoid insertUser(Useruser)throws Exception; }

Publicclass UserDaoImpl implements UserDao {

javaEE框架课程

数 }

//注入SqlSessionFactory

public UserDaoImpl(SqlSessionFactory sqlSessionFactory){ this.setSqlSessionFactory(sqlSessionFactory); }

private SqlSessionFactory sqlSessionFactory; @Override

public UsergetUserById(int id) throws Exception {

SqlSession session = sqlSessionFactory.openSession(); Useruser =null; try {

//通过sqlsession调用selectOne方法获取一条结果集

//参数1:指定定义的statement的id,参数2:指定向statement中传递的参

user = session.selectOne(\, 1); System.out.println(user); } finally{

session.close(); }

returnuser; }

@Override

Publicvoid insertUser(Useruser)throws Exception {

SqlSession sqlSession= sqlSessionFactory.openSession(); try {

sqlSession.insert(\, user); sqlSession.commit(); } finally{

session.close(); } }

2.3.3 问题

原始Dao开发中存在以下问题:

? Dao方法体存在重复代码:通过SqlSessionFactory创建SqlSession,调用SqlSession的数

据库操作方法

? 调用sqlSession的数据库操作方法需要指定statement的id,这里存在硬编码,不

得于开发维护。

javaEE框架课程

3.3 settings(配置)

mybatis全局配置参数,全局参数将会影响mybatis的运行行为。

详细参见“学习资料/mybatis-settings.xlsx”文件

javaEE框架课程

3.4 typeAliases(类型别名) 3.4.1 mybatis支持别名:

别名 _byte _long _short _int _integer _double _float _boolean string byte long short int integer double float boolean date decimal bigdecimal

映射的类型 byte long short int int double float boolean String Byte Long Short Integer Integer Double Float Boolean Date

BigDecimal BigDecimal

3.4.2 自定义别名:

在SqlMapConfig.xml中配置:

javaEE框架课程

3.5 typeHandlers(类型处理器)

类型处理器用于java类型和jdbc类型映射,如下:

select * from userwhere id = #{id}

mybatis自带的类型处理器基本上满足日常需求,不需要单独定义。

mybatis支持类型处理器:

类型处理器

BooleanTypeHandler ByteTypeHandler ShortTypeHandler IntegerTypeHandler LongTypeHandler FloatTypeHandler DoubleTypeHandler BigDecimalTypeHandler StringTypeHandler ClobTypeHandler NStringTypeHandler NClobTypeHandler ByteArrayTypeHandler BlobTypeHandler DateTypeHandler DateOnlyTypeHandler TimeOnlyTypeHandler SqlTimestampTypeHandler SqlDateTypeHandler SqlTimeTypeHandler ObjectTypeHandler EnumTypeHandler

Java类型

Boolean,boolean Byte,byte Short,short Integer,int Long,long Float,float Double,double BigDecimal String String String String byte[] byte[]

Date(java.util) Date(java.util) Date(java.util)

Timestamp(java.sql) Date(java.sql) Time(java.sql) 任意

Enumeration类型

JDBC类型

任何兼容的布尔值

任何兼容的数字或字节类型 任何兼容的数字或短整型 任何兼容的数字和整型 任何兼容的数字或长整型 任何兼容的数字或单精度浮点型

任何兼容的数字或双精度浮点型

任何兼容的数字或十进制小数类型

CHAR和VARCHAR类型 CLOB和LONGVARCHAR类型

NVARCHAR和NCHAR类型 NCLOB类型

任何兼容的字节流类型 BLOB和

LONGVARBINARY类型 TIMESTAMP类型 DATE类型 TIME类型

TIMESTAMP类型 DATE类型 TIME类型

其他或未指定类型

VARCHAR-任何兼容的字符串类型,作为代码存储(而不是索引)。

javaEE框架课程

3.6 mappers(映射器)

Mapper配置的几种方法:

3.6.1

使用相对于类路径的资源

如:

3.6.2

使用完全限定路径

如:

3.6.3

使用mapper接口类路径

如:

注意:此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。

3.6.4

注册指定包下的所有mapper接口

如:

注意:此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。

4 Mapper.xml映射文件

Mapper.xml映射文件中定义了操作数据库的sql,每个sql是一个statement,映射文件是mybatis的核心。

javaEE框架课程

4.1 parameterType(输入类型)

4.1.1 #{}与${}

#{}实现的是向prepareStatement中的预处理语句中设置参数值,sql语句中#{}表示一个占位符即?。

select * from userwhere id = #{id}

使用占位符#{}可以有效防止sql注入,在使用时不需要关心参数值的类型,mybatis会自动进行java类型和jdbc类型的转换。#{}可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,#{}括号中可以是value或其它名称。

${}和#{}不同,通过${}可以将parameterType 传入的内容拼接在sql中且不进行jdbc类型转换, ${}可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,${}括号中只能是value。使用${}不能防止sql注入,但是有时用${}会非常方便,如下的例子:

select * from userwhere username like '%${value}%'

如果本例子使用#{}则传入的字符串中必须有%号,而%是人为拼接在参数中,显然有点麻烦,如果采用${}在sql中拼接为%的方式则在调用mapper接口传递参数就方便很多。

//如果使用占位符号则必须人为在传参数中加%

List list = userMapper.selectUserByName(\管理员%\);

//如果使用${}原始符号则不用人为在参数中加%

Listlist = userMapper.selectUserByName(\管理员\);

再比如order by排序,如果将列名通过参数传入sql,根据传的列名进行排序,应该写为: ORDER BY ${columnName}

如果使用#{}将无法实现此功能。

javaEE框架课程

4.2.4 resultType总结:

输出pojo对象和输出pojo列表在sql中定义的resultType是一样的。

返回单个pojo对象要保证sql查询出来的结果集为单条,内部使用session.selectOne方法调用,mapper接口使用pojo对象作为方法返回值。

返回pojo列表表示查询出来的结果集可能为多条,内部使用session.selectList方法,mapper接口使用List对象作为方法返回值。

4.2.5 输出hashmap

输出pojo对象可以改用hashmap输出类型,将输出的字段名称作为map的key,value为字段值。

4.3 resultMap

resultType可以指定pojo将查询结果映射为pojo,但需要pojo的属性名和sql查询的列名一致方可映射成功。 如果sql查询字段名和pojo的属性名不一致,可以通过resultMap将字段名和属性名作一个对应关系,resultMap实质上还需要将查询结果映射到pojo对象中。 resultMap可以实现将查询结果映射为复杂类型的pojo,比如在查询结果映射对象中包括pojo和list实现一对一查询和一对多查询。

4.3.1 Mapper.xml定义

使用resultMap指定上边定义的personmap。

javaEE框架课程

4.3.2 定义resultMap

由于上边的mapper.xml中sql查询列和Users.java类属性不一致,需要定义resultMap:userListResultMap将sql查询列和Users.java类属性对应起来

:此属性表示查询结果集的唯一标识,非常重要。如果是多个字段为复合唯一约束则定义多个

Property:表示person类的属性。 Column:表示sql查询出来的字段名。

Column和property放在一块儿表示将sql查询出来的字段映射到指定的pojo类属性上。

:普通结果,即pojo的属性。

4.3.3 Mapper接口定义

public ListfindUserListResultMap() throws Exception;

4.4 动态sql(重点)

通过mybatis提供的各种标签方法实现动态拼接sql。

javaEE框架课程

4.4.1 If

select * from user where 1=1

and id=#{id}

and username like '%${username}%'

注意要做不等于空字符串校验。

4.4.2 Where

上边的sql也可以改为:

select * from user

and id=#{id}

and username like '%${username}%'

可以自动处理第一个and。

4.4.3 foreach

向sql传递数组或List,mybatis使用foreach解析,如下:

javaEE框架课程

4.4.3.1 通过pojo传递list

? 需求

传入多个id查询用户信息,用下边两个sql实现:

SELECT * FROM USERS WHERE username LIKE '%张%' AND (id =10 OR id =89 OR id=16) SELECT * FROM USERS WHERE username LIKE '%张%' id IN (10,89,16)

? 在pojo中定义list属性ids存储多个用户id,并添加getter/setter方法

? mapper.xml

#{id}

? 测试代码:

List ids = new ArrayList();

ids.add(1);//查询id为1的用户 ids.add(10);//查询id为10的用户 queryVo.setIds(ids);

List list = userMapper.findUserList(queryVo);

javaEE框架课程

4.4.3.2 传递单个List

传递List类型在编写mapper.xml没有区别,唯一不同的是只有一个List参数时它的参数名为list。

如下:

? Mapper.xml

select * from user

#{item.id}

? Mapper接口

public List selectUserByList(List userlist) throws Exception;

? 测试:

Publicvoid testselectUserByList()throws Exception{

//获取session

SqlSession session = sqlSessionFactory.openSession(); //获限mapper接口实例

UserMapper userMapper = session.getMapper(UserMapper.class); //构造查询条件List

List userlist = new ArrayList(); Useruser =new User(); user.setId(1);

userlist.add(user); user = new User(); user.setId(2);

userlist.add(user);

//传递userlist列表查询用户列表

Listlist = userMapper.selectUserByList(userlist); //关闭session

本文来源:https://www.bwwdw.com/article/4736.html

Top