01.MyBatis框架介绍

一、企业级应用

企业级应用是指那些为商业组织、大型企业而创建并部署的解决方案及应用。这些大型企业级应用的结构复杂,涉及的外部资源众多、事务密集、数据量大、用户数多,有较强的安全性考虑。

企业级开发主要是针对企业级应用的开发。作为企业级应用,其不但要有强大的功能,还要能够满足未来业务需求的变化,易于升级和维护。

在规模较大的Java企业级应用开发中,直接使用JDBC API进行数据访问层开发存在诸多不便,所以我们要使用MyBatis框架简化数据访问层的开发。 

二、框架的概念

框架,即Framework。该词最早出现在建筑领域,指的是在建造房屋前期构建的建筑骨架。

在编程领域,框架就是应用程序的骨架,开发人员可以在这个骨架上加入自己的东西,搭建出符合自己需求的应用系统。

其实就是某种应用的半成品,是一组组件,但是这个东西复用性特别强,可以让广大程序开发人员完成自己的系统。

简而言之,框架就是可以被应用开发者定制的应用骨架。而且,框架一般是成熟的,不断升级的软件。 

框架的优势

1.使用JSP+Servlet技术进行JavaEE应用的开发有两个弊端:

  • 软件应用和系统可维护性差
  • 代码重用性低

2.相比于使用JSP+Servlet技术进行软件开发,使用框架有以下优势:

  • 提高开发效率
  • 提高代码规范性和可维护性
  • 提高软件性能

三、主流框架技术简介

目前JavaEE企业级开发的三大主流框架是SSM(Spring,SpringMVC,Mybatis)。

Spring框架

Spring是一个开源框架,是为了解决企业应用程序开发复杂性而创建的,其主要优势之一就是分层架构。Spring提供了更完善的开发环境,可以为POJO(Plain Ordinary Java Object,普通Java对象)对象提供企业级的服务。

Spring MVC框架

Spring MVC是一个Web开发框架,可以将它理解为Servlet。在MVC模式中,Spring MVC作为控制器(Controller)用于实现模型与视图的数据交互,是结构最清晰的。Spring MVC框架采用松耦合、可插拔的组件结构,具有高度可配置性,与其他的MVC框架相比,具有更强的扩展性和灵活性。

MyBatis框架

MyBatis是Apache的一个开源项目iBatis,2010年这个项目由Apache Software Foundation迁移到了Google Code,并且改名为MyBatis,2013年11月MyBatis又被迁移到Github。MyBatis是一个优秀的持久层框架,它可以在实体类和SQL语句之间建立映射关系,是一种半自动化的ORM(Object/Relation Mapping,即对象关系映射)实现。MyBatis封装性要低于Hibernate,但它性能优越、简单易学,在互联网应用的开发中被广泛使用。 

四、MyBatis框架

(一)传统JDBC的劣势

1.影响系统性能:数据库连接创建、释放频繁会造成系统资源浪费,从而影响系统性能。

2.不易维护:

(1)SQL语句在代码中硬编码,造成代码不易维护。在实际应用的开发中,SQL变化的可能性较大。在传统JDBC编程中,SQL变动需要改变Java代码,不易维护。

(2)使用PreparedStatement向占位符传参数存在硬编码,因为SQL语句的where条件不一定,可能多也可能少,修改SQL需要修改Java代码,不易维护。

(3)JDBC对结果集解析存在硬编码(查询列名),SQL变化会导致解析的Java代码变化,不易维护。

(二)MyBatis概述

 

1.MyBatis框架是一个ORM(Object Relation Mapping,即对象关系映射)框架。

2.所谓的ORM就是一种为了解决面向对象关系型数据库中数据类型不匹配的技术,它通过描述Java对象数据库表之间的映射关系,自动将Java应用程序中的对象持久化到关系型数据库的表中。

ORM 把数据库映射为对象:

  • 数据表(table)→ 类(class)
  • 数据行(record,记录)→ 对象(object)
  • 字段(field)→ 对象的属性(attribute)

⼀般的 ORM 框架,会将数据库模型的每张表都映射为⼀个 Java 类。

3.针对上面提到的传统JDBC编程的劣势,MyBatis提供了以下解决方案:

问题一:数据库连接创建、释放频繁会造成系统资源浪费,从而影响系统性能。

解决方案:在mybatis-config.xml中配置数据连接池,使用连接池管理数据库连接。

问题二:SQL语句在代码中硬编码,造成代码不易维护。在实际应用的开发中,SQL变化的可能性较大。在传统JDBC编程中,SQL变动需要改变Java代码,不易维护。

解决方案:MyBatis将SQL语句配置在MyBatis的映射文件中,实现了SQL语句与Java代码的分离。

问题三:使用PreparedStatement向占位符传参数存在硬编码,因为SQL语句的where条件不一定,可能多也可能少,修改SQL需要修改Java代码,不易维护。

解决方案:MyBatis自动将Java对象映射至SQL语句,通过Statement中的parameterType定义输入参数的类型。

问题四:JDBC对结果集解析存在硬编码(查询列名),SQL变化会导致解析的Java代码变化,不易维护。

解决方案:MyBatis自动将SQL执行结果映射至Java对象,通过Statement中的resultType定义输出结果的类型。

(三)MyBatis环境搭建

1.使用步骤

在项目中使用MyBatis框架可以按照以下几个步骤进行:

(1)创建项目,添加所需要的jar文件,可以下载jar后添加,也可以添加依赖引用。

(2)编写MyBatis框架的核心配置文件。

(3)创建实体类。

(4)创建Mapper接口。

(5)创建SQL映射文件。

(6)编写业务逻辑代码。

2.开始实践

(1)数据准备

参见JavaWeb通过JDBC连接MySQL数据库 中“0、数据准备”部分。

(2)创建项目

使用IDEA创建一个JavaWeb项目。项目名称填写“MyBatisTest”,构建系统选择【Maven】,组ID填写“com.sdbi”。

添加“Web应用程序”框架支持。

  

运行项目测试是否成功。

     

运行Tomcat,浏览器地址栏输入http://localhost:8080/MyBatisTest/,看到成功页面,说明项目创建成功。

  

(3)引入相关依赖

需要导入MySQL驱动包,MyBatis核心包。

Maven方式:

在项目的pom.xml文件中添加如下代码:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <project xmlns="http://maven.apache.org/POM/4.0.0"
 3          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5     <modelVersion>4.0.0</modelVersion>
 6 
 7     <groupId>com.sdbi</groupId>
 8     <artifactId>MyBatisTest</artifactId>
 9     <version>1.0-SNAPSHOT</version>
10 
11     <properties>
12         <maven.compiler.source>8</maven.compiler.source>
13         <maven.compiler.target>8</maven.compiler.target>
14         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
15     </properties>
16 
17     <dependencies>
18         <dependency>
19             <groupId>mysql</groupId>
20             <artifactId>mysql-connector-java</artifactId>
21             <version>5.1.49</version>
22         </dependency>
23         <dependency>
24             <groupId>org.mybatis</groupId>
25             <artifactId>mybatis</artifactId>
26             <version>3.5.9</version>
27         </dependency>
28     </dependencies>
29 </project>

上述代码中,第18~22行是MySQL驱动包,第23~27行是MyBatis核心包。

保存pom.xml文件后,点击右侧悬浮的同步按钮,如果同步缓慢或者不成功,我们可以使用本地Maven。

在<mirrors>节点内,添加如下代码,使用阿里云的Maven。

1 <mirror>
2   <id>aliyunmaven</id>
3   <mirrorOf>*</mirrorOf>
4   <name>阿里云公共仓库</name>
5   <url>https://maven.aliyun.com/repository/public</url>
6 </mirror>

 

 到IDEA设置中,找到【构建、执行、部署】->【构建工具】->【Maven】,将【Maven主路径】修改为本机安装的Maven路径。 

我们使用Maven方式添加jar包,在发布项目之前一定要在【项目结构】--【工件】中将Maven方式导入的包(右侧)添加到【输出根】(左侧)中。

添加jar包方式:

下载mysql驱动包和MyBatis包(https://repo.maven.apache.org/maven2/org/mybatis/mybatis/3.5.9/mybatis-3.5.9.jar)

在web文件夹下新建lib文件夹,将jar包复制粘贴到lib文件夹下,

右键jar包,选择【添加为库】

到【文件】->【项目结构】中查看一下,已经添加成功。

 (4)创建数据库连接配置文件

src/main/resources目录下创建db.properties文件,在该文件中配置数据库连接的参数。

mysql.driver=com.mysql.jdbc.Driver
mysql.url=jdbc:mysql://localhost:3306/db_test?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
mysql.username=root
mysql.password=1234

(5)创建MyBatis核心配置文件

src/main/resources目录下创建mybatis-config.xml文件,该文件主要用于项目的环境配置。

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 3         "http://mybatis.org/dtd/mybatis-3-config.dtd">
 4 <configuration>
 5     <!-- 环境配置 -->
 6     <!-- 加载类路径下的属性文件 -->
 7     <properties resource="db.properties"/>
 8     <environments default="development">
 9         <environment id="development">
10             <transactionManager type="JDBC"/>
11             <!-- 数据库连接配置,db.properties文件中的内容 -->
12             <dataSource type="POOLED">
13                 <property name="driver" value="${mysql.driver}"/>
14                 <property name="url" value="${mysql.url}"/>
15                 <property name="username" value="${mysql.username}"/>
16                 <property name="password" value="${mysql.password}"/>
17             </dataSource>
18         </environment>
19     </environments>
20 </configuration>

至此,MyBatis的开发环境就搭建完成了。

(四)第一个MyBatis程序

我们来试一下刚刚配置好的MyBatis开发环境。

1.创建POJO实体

src/maim/java目录下创建com.sdbi.pojo包,在该包下创建User类。

package com.sdbi.pojo;

public class User {
    private int id;
    private String name;
    private String password;
    private String email;
    private String birthday;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getBirthday() {
        return birthday;
    }

    public void setBirthday(String birthday) {
        this.birthday = birthday;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", email='" + email + '\'' +
                ", birthday='" + birthday + '\'' +
                '}';
    }
}

2.创建映射文件UserMapper.xml

src/maim/resources目录下创建一个mapper文件夹,在mapper文件夹下创建映射文件UserMapper.xml。映射文件通常用POJO实体类名+Mapper命名。

注意:第5行<mapper>标签中一定要添加namespace属性。我们这里没有使用Mapper代理接口,所以namespace是我们定义的POJO类。

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 3         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 4 <!-- mapper为映射根节点 -->
 5 <mapper namespace="com.sdbi.pojo.User">
 6     <!-- id是接口中的“方法名”,parameterType是传入参数的数据类型,resultType是返回的实体类名(包名.类名)-->
 7     <select id="findByName" parameterType="String" resultType="com.sdbi.pojo.User">
 8         select *
 9         from tb_users
10         where name = #{name}
11     </select>
12     <select id="findById" parameterType="Integer" resultType="com.sdbi.pojo.User">
13         select *
14         from tb_users
15         where id = #{id}
16     </select>
17 </mapper>

第7~11行定义了通过name查询的<select>标签,第7~11行定义了通过name查询的<select>标签。

3.修改mybatis-config.xml配置文件

在mybatis-config.xml配置文件的第19行代码后,添加UserMapper.xml映射文件路径的配置,用于将UserMapper.xml映射文件加载到程序中。

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 3         "http://mybatis.org/dtd/mybatis-3-config.dtd">
 4 <configuration>
 5     <!-- 环境配置 -->
 6     <!-- 加载类路径下的属性文件 -->
 7     <properties resource="db.properties"/>
 8     <environments default="development">
 9         <environment id="development">
10             <transactionManager type="JDBC"/>
11             <!-- 数据库连接配置,db.properties文件中的内容 -->
12             <dataSource type="POOLED">
13                 <property name="driver" value="${mysql.driver}"/>
14                 <property name="url" value="${mysql.url}"/>
15                 <property name="username" value="${mysql.username}"/>
16                 <property name="password" value="${mysql.password}"/>
17             </dataSource>
18         </environment>
19     </environments>
20 
21     <mappers>
22         <mapper resource="mapper/UserMapper.xml"/>
23     </mappers>
24 </configuration>

4.编写Servlet类

先添加servlet-api.jar依赖

可以通过添加依赖的方式:

1 <dependency>
2     <groupId>javax.servlet</groupId>
3     <artifactId>javax.servlet-api</artifactId>
4     <version>4.0.1</version>
5     <scope>provided</scope>
6 </dependency>

也可以导入jar包:

编写Servlet类,命名为UserServlet。一定注意要添加@WebServlet("/UserServlet")。 
 1 package com.sdbi.servlet;
 2 
 3 import com.sdbi.pojo.User;
 4 import org.apache.ibatis.io.Resources;
 5 import org.apache.ibatis.session.SqlSession;
 6 import org.apache.ibatis.session.SqlSessionFactory;
 7 import org.apache.ibatis.session.SqlSessionFactoryBuilder;
 8 
 9 import javax.servlet.ServletException;
10 import javax.servlet.annotation.WebServlet;
11 import javax.servlet.http.HttpServlet;
12 import javax.servlet.http.HttpServletRequest;
13 import javax.servlet.http.HttpServletResponse;
14 import java.io.IOException;
15 import java.io.Reader;
16 
17 @WebServlet("/UserServlet")
18 public class UserServlet extends HttpServlet {
19     @Override
20     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
21         System.out.println("doGet()...start");
22         resp.setContentType("text/html;charset=utf-8");
23         String resouces = "mybatis-config.xml";
24         // 创建流
25         Reader reader = null;
26 
27         try {
28             // 读取mybatis-config.xml文件内容到reader对象中
29             reader = Resources.getResourceAsReader(resouces);
30             // 初始化MyBatis数据源,创建SqlSessionFactory类的实例
31             SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
32             // 创建SqlSession类的实例
33             SqlSession session = sqlSessionFactory.openSession();
34             // 传入参数,执行findByName查询,返回结果User
35             User user = session.selectOne("findByName", "zhangsan");
36 //            User user = session.selectOne("findById", 2);
37             // 输出结果
38             if (user != null) {
39                 System.out.println(user.toString());
40                 resp.getWriter().println(user.toString());
41             } else {
42                 System.out.println("not found.");
43                 resp.getWriter().println("not found.");
44             }
45             // 关闭SqlSession对象
46             session.close();
47         } catch (Exception e) {
48             e.printStackTrace();
49         }
50         System.out.println("doGet()...end");
51     }
52 }

第29行用于读取mybatis-config.xml文件内容到reader对象中;

第31~33行用于创建SqlSessionFactory类的实例,并通过SqlSessionFactory类的实例创建SqlSession类的实例;

第35行调用selectOne()方法,查询name为“zhangsan”的用户信息,并将查询结果返回给User对象。

或者,第36行调用selectOne()方法,查询id为2的用户信息,并将查询结果返回给User对象。

运行结果如下:

错误解决

错误现象:

如果使用Maven通过依赖添加MyBatis的jar包,在运行时会出现如下错误。

15-Mar-2023 15:42:52.022 严重 [http-nio-8080-exec-4] org.apache.catalina.core.StandardWrapperValve.invoke 在路径为/MyBatisTest的上下文中,Servlet[com.sdbi.servlet.UserServlet]的Servlet.service()引发了具有根本原因的异常Servlet执行抛出一个异常
	java.lang.ClassNotFoundException: org.apache.ibatis.io.Resources
		at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1420)
		at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1228)
		at com.sdbi.servlet.UserServlet.doGet(UserServlet.java:31)
		at javax.servlet.http.HttpServlet.service(HttpServlet.java:656)

原因:没有在设置中将项目设置为输出目录。

 

在工件设置中,将项目设置为输出目录。

(五)IDEA中创建XML模板

注意:对于经常使用的XML文件我们可以在IDEA中创建模板。

1.【文件】->【设置】->【编辑器】->【文件和代码模板】

2.填写模板名称为“mybatis-mapper.xml”,扩展名为“xml”,内容填写基本的代码部分。

模板代码:

1 <?xml version="1.0" encoding="UTF-8" ?>
2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
3         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
4 <mapper namespace="">
5 
6 </mapper>

 

3.使用模板

 

(六)MyBatis的面向接口编程

我们还可以使用MyBatis的面向接口编程。

MyBatis的面向接口编程的作用就是将所有的数据库操作交由mapper的配置来完成,而不需要人工的在dao层写数据库操作的代码。

我们先来定义Dao层接口,该接口的名字可以是以Dao结尾,也可以是以Mapper结尾,例如,UserDao(包名com.sdbi.dao),UserMapper(包名com.sdbi.mapper)。

1.新建com.sdbi.mapper包,在包里新建一个接口UserMapper,代码如下:

package com.sdbi.mapper;

import com.sdbi.pojo.User;

public interface UserMapper {
    public User findByName(String name);

    public User findById(Integer id);
}

2.修改UserMapper.xml第5行的namespace属性,改为刚刚定义的接口:

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 3         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 4 <!-- mapper为映射根节点 -->
 5 <mapper namespace="com.sdbi.mapper.UserMapper">
 6     <!-- id是接口中的“方法名”,parameterType是传入参数的数据类型,resultType是返回的实体类名(包名.类名)-->
 7     <select id="findByName" parameterType="String" resultType="com.sdbi.pojo.User">
 8         select *
 9         from tb_users
10         where name = #{name}
11     </select>
12     <select id="findById" parameterType="Integer" resultType="com.sdbi.pojo.User">
13         select *
14         from tb_users
15         where id = #{id}
16     </select>
17 </mapper>

3.修改UserServlet类的第35~38行:

 1 package com.sdbi.servlet;
 2 
 3 import com.sdbi.mapper.UserMapper;
 4 import com.sdbi.pojo.User;
 5 import org.apache.ibatis.io.Resources;
 6 import org.apache.ibatis.session.SqlSession;
 7 import org.apache.ibatis.session.SqlSessionFactory;
 8 import org.apache.ibatis.session.SqlSessionFactoryBuilder;
 9 
10 import javax.servlet.ServletException;
11 import javax.servlet.annotation.WebServlet;
12 import javax.servlet.http.HttpServlet;
13 import javax.servlet.http.HttpServletRequest;
14 import javax.servlet.http.HttpServletResponse;
15 import java.io.IOException;
16 import java.io.Reader;
17 
18 @WebServlet("/UserServlet")
19 public class UserServlet extends HttpServlet {
20     @Override
21     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
22         System.out.println("doGet()...start");
23         resp.setContentType("text/html;charset=utf-8");
24         String resouces = "mybatis-config.xml";
25         // 创建流
26         Reader reader = null;
27 
28         try {
29             // 读取mybatis-config.xml文件内容到reader对象中
30             reader = Resources.getResourceAsReader(resouces);
31             // 初始化MyBatis数据源,创建SqlSessionFactory类的实例
32             SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
33             // 创建SqlSession类的实例
34             SqlSession session = sqlSessionFactory.openSession();
35             // 调用SqlSession的getMapper方法,获取指定接口的实现类对象
36             UserMapper userMapper = session.getMapper(UserMapper.class);
37             // 传入参数,调用实现类对象的方法,返回结果User
38             User user = userMapper.findById(3);
39             // 输出结果
40             if (user != null) {
41                 System.out.println(user.toString());
42                 resp.getWriter().println(user.toString());
43             } else {
44                 System.out.println("not found.");
45                 resp.getWriter().println("not found.");
46             }
47             // 关闭SqlSession对象
48             session.close();
49         } catch (Exception e) {
50             e.printStackTrace();
51         }
52         System.out.println("doGet()...end");
53     }
54 }

MyBatis框架抛开了Dao的实现类,直接定位到映射文件mapper中的相应SQL语句,对DB进行操作。这种对Dao的实现方式称为Mapper的动态代理方式。

Mapper动态代理方式无需实现 Dao 接口。接口是由 MyBatis 结合映射文件自动生成的动态代理实现的。

相关接口和映射文件之间的规则:

(1)在mapper.xml中将namespace设置为对应的mapper.java(Dao接口)的全限定名。

(2)将mapper.java接口的方法名和mapper.xml中statement的id保持一致。

(3)将mapper.java接口的方法输入参数类型和mapper.xml中statement的parameterType保持一致。

(4)将mapper.java接口的方法输出结果类型和mapper.xml中statement的resultType保持一致。

Dao 接口不需要实现类

MyBatis在采用面向接口编程时,采用实体+接口+映射文件的方式。其中接口是不需要实现类的。

因为Mybatis提供了Mapper接口的代理对象(MyBatis通过JDK的动态代理方式,在启动加载配置文件时,根据配置mapper的xml去生成Dao的实现,session.getMapper()使用了代理,当调用一次此方法,都会产生一个代理class的instance。)

在执行Mapper接口方法时,实际执行的是MyBatis的代理对象,代理对象在 invoke 方法内获取 Mapper接口类全名+方法全名 作为statement的id,然后通过id去statement匹配注册的SQL,然后使用 SqlSession 执行这个 SQL。所以,这也解释了为什么Mybatis映射文件需要 namespace 和 id ,前者是类全名,后者是方法名。

(七)MyBatis工作原理

MyBatis框架在操作数据库时,大体经过了8个步骤。下面结合MyBatis工作原理图对每一步流程进行详细讲解,具体如下。
(1)MyBatis读取核心配置文件mybatis-config.xml:mybatis-config.xml核心配置文件主要配置了MyBatis的运行环境等信息。
(2)加载映射文件Mapper.xml:Mapper.xml文件即SQL映射文件,该文件配置了操作数据库的SQL语句,需要在mybatis-config.xml中加载才能执行。
(3)构造会话工厂:通过MyBatis的环境等配置信息构建会话工厂SqlSessionFactory,用于创建SqlSession。
(4)创建会话对象:由会话工厂SqlSessionFactory创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。
(5)创建执行器:会话对象本身不能直接操作数据库,MyBatis底层定义了一个Executor接口用于操作数据库,执行器会根据SqlSession传递的参数动态的生成需要执行的SQL语句,同时负责查询。
(6)封装SQL信息:SqlSession内部通过执行器Executor操作数据库,执行器将待处理的SQL信息封装到MappedStatement对象中。
(7)操作数据库:根据动态生成的SQL操作数据库。
(8)输出结果映射:执行SQL语句之后,通过MappedStatement对象将输出结果映射至Java对象中。

 

posted @ 2023-03-06 08:03  熊猫Panda先生  阅读(2051)  评论(0编辑  收藏  举报