Loading [MathJax]/jax/output/HTML-CSS/config.js
SpringBoot入门教程(二):Mybatis

5.Mybatis 入门

5.1 快速入门程序

5.1.1 准备工作

创建 springboot 工程

mybatis1.png

mybatis2.png

pom.xml 中的依赖如下:

<dependencies>
<!-- mybatis起步依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<!-- mysql驱动包依赖 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- spring单元测试 (集成了junit) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

数据准备

创建用户表 User,并创建对应的实体类 User。

-- 用户表
create table user(
id int unsigned primary key auto_increment comment 'ID',
name varchar(100) comment '姓名',
age tinyint unsigned comment '年龄',
gender tinyint unsigned comment '性别, 1:男, 2:女',
phone varchar(11) comment '手机号'
) comment '用户表';
-- 测试数据
insert into user(id, name, age, gender, phone) VALUES (null,'白眉鹰王',55,'1','18800000000');
insert into user(id, name, age, gender, phone) VALUES (null,'金毛狮王',45,'1','18800000001');
insert into user(id, name, age, gender, phone) VALUES (null,'青翼蝠王',38,'1','18800000002');
insert into user(id, name, age, gender, phone) VALUES (null,'紫衫龙王',42,'2','18800000003');
insert into user(id, name, age, gender, phone) VALUES (null,'光明左使',37,'1','18800000004');
insert into user(id, name, age, gender, phone) VALUES (null,'光明右使',48,'1','18800000005');

实体类的属性名与表中的字段名一一对应。

public class User {
private Integer id; //id(主键)
private String name; //姓名
private Short age; //年龄
private Short gender; //性别
private String phone; //手机号
//省略GET, SET方法
}

5.1.2 配置 Mybatis

连接数据库的四大参数:MySQL 驱动类,登录名,密码和数据库连接字符串。

在 springboot 项目中,可以编写 application.properties 文件,配置数据库连接信息。我们要连接数据库,就需要配置数据库连接的基本信息,包括:driver-class-name、url 、username,password。

#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/db_mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=1234

5.1.3 编写 SQL 语句

在创建出来的 springboot 工程中,在引导类所在包下,再创建一个包 mapper。在 mapper 包下创建一个接口 UserMapper,这是一个持久层接口(Mybatis 的持久层接口规范一般都叫 XXXMapper)。

mybatis3.png

import com.ayanokouji.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface UserMapper {
// 查询所有用户信息
@Select("select id, name, age, gender, phone from user")
public List<User> list();
}

@Mapper 注解:表示是 mybatis 的 Mapper 接口。程序运行时,框架会自动生成接口的实现类对象(代理对象),并交给 Spring 的 IOC 容器管理。

@Select 注解:代表的就是 select 查询,用于书写 select 查询语句。

5.1.4 单元测试

在创建出来的 SpringBoot 工程中,在 src 下的 test 目录下,已经自动帮我们创建好了测试类 ,并且在测试类上已经添加了注解 @SpringBootTest,代表该测试类已经与 SpringBoot 整合。

该测试类在运行时,会自动通过引导类加载 Spring 的环境(IOC 容器)。我们要测试那个 bean 对象,就可以直接通过 @Autowired 注解直接将其注入进行,然后就可以测试了。

@SpringBootTest
class MybaitsQuickstartApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
public void testList(){
List<User> users = userMapper.list();
for (User user : users) {
System.out.println(user);
}
}
}

运行结果如下:

User{id=1, name='白眉鹰王', age=55, gender=1, phone='18800000000'}
User{id=2, name='金毛狮王', age=45, gender=1, phone='18800000001'}
User{id=3, name='青翼蝠王', age=38, gender=1, phone='18800000002'}
User{id=4, name='紫衫龙王', age=42, gender=2, phone='18800000003'}
User{id=5, name='光明左使', age=37, gender=1, phone='18800000004'}
User{id=6, name='光明右使', age=48, gender=1, phone='18800000005'}

5.2 数据库连接池

数据库连接池是个容器,负责分配、管理数据库连接:

  • 在程序启动时,会在数据库连接池中,创建一定数量的 Connection 对象

允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个

  • 客户端在执行 SQL 时,先从连接池获取一个 Connection 对象,然后再执行 SQL 语句,SQL 语句执行完之后,释放 Connection 时就会把 Connection 随想归还给连接池(Connection 对象可以复用)

释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏

  • 客户端获取到 Connection 对象了,但是 Connection 对象并没有去访问数据库(处于空闲),数据库连接池发现 Connection 兑现的空闲时间》连接池中预设的最大空闲时间,此时数据库连接池就会自动释放掉这个连接对象

数据库连接池的好处:

  1. 资源重用
  2. 提升系统响应速度
  3. 避免数据库连接遗漏

常见的数据库连接池:

  • C3P0
  • DBCP
  • Druid
  • Hikari(Springboot 默认)

现在使用更多的是:Hikari(追光者)、Druid(德鲁伊)。

如果我们想把默认的数据库连接池切换为 Druid 数据库连接池,只需要完成以下两步操作即可:

  1. 在 pom.xml 文件中引入依赖

    com.alibaba
    druid-spring-boot-starter
    1.2.8
  2. 在 application.properties 中引入数据库连接配置

    方式 1:

    spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.druid.url=jdbc:mysql://localhost:3306/db_mybatis
    spring.datasource.druid.username=root
    spring.datasource.druid.password=1234

    方式 2:

    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/db_mybatis
    spring.datasource.username=root
    spring.datasource.password=1234

5.3 lombok

Lombok 是一个实用的 Java 类库,可以通过简单的注解来简化和消除一些必须有但显得很臃肿的 Java 代码,可以使得实体类的编写更加简洁。

注解 作用
@Getter/@Setter 为所有的属性提供 get/set 方法
@ToString 会给类自动生成易阅读的 toString 方法
@EqualsAndHashCode 根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法
@Data 提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode)
@NoArgsConstructor 为实体类生成无参的构造器方法
@AllArgsConstructor 为实体类生成除了 static 修饰的字段之外带有各参数的构造器方法。

第一步,在 pom.xml 中引入依赖

<!-- 在springboot的父工程中,已经集成了lombok并指定了版本号,故当前引入依赖时不需要指定version -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

第二步,在实体类上添加注解

@Data //getter方法、setter方法、toString方法、hashCode方法、equals方法
@NoArgsConstructor //无参构造
@AllArgsConstructor//全参构造
public class User {
private Integer id; //id(主键)
private String name; //姓名
private Short age; //年龄
private Short gender; //性别
private String phone; //手机号
}

6. Mybatis

6.1 准备工作

实施前的准备工作:

  1. 准备数据库表
  2. 创建一个新的 springboot 工程,选择引入对应的起步依赖(mybatis、mysql 驱动、lombok)
  3. application.properties 中引入数据库连接信息
  4. 创建对应的实体类 Emp(实体类属性采用驼峰命名)
  5. 准备 Mapper 接口 EmpMapper

准备数据库表

-- 部门管理
create table dept
(
id int unsigned primary key auto_increment comment '主键ID',
name varchar(10) not null unique comment '部门名称',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '部门表';
-- 部门表测试数据
insert into dept (id, name, create_time, update_time)
values (1, '学工部', now(), now()),
(2, '教研部', now(), now()),
(3, '咨询部', now(), now()),
(4, '就业部', now(), now()),
(5, '人事部', now(), now());
-- 员工管理
create table emp
(
id int unsigned primary key auto_increment comment 'ID',
username varchar(20) not null unique comment '用户名',
password varchar(32) default '123456' comment '密码',
name varchar(10) not null comment '姓名',
gender tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
image varchar(300) comment '图像',
job tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师',
entrydate date comment '入职时间',
dept_id int unsigned comment '部门ID',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '员工表';
-- 员工表测试数据
INSERT INTO emp (id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time)
VALUES
(1, 'jinyong', '123456', '金庸', 1, '1.jpg', 4, '2000-01-01', 2, now(), now()),
(2, 'zhangwuji', '123456', '张无忌', 1, '2.jpg', 2, '2015-01-01', 2, now(), now()),
(3, 'yangxiao', '123456', '杨逍', 1, '3.jpg', 2, '2008-05-01', 2, now(), now()),
(4, 'weiyixiao', '123456', '韦一笑', 1, '4.jpg', 2, '2007-01-01', 2, now(), now()),
(5, 'changyuchun', '123456', '常遇春', 1, '5.jpg', 2, '2012-12-05', 2, now(), now()),
(6, 'xiaozhao', '123456', '小昭', 2, '6.jpg', 3, '2013-09-05', 1, now(), now()),
(7, 'jixiaofu', '123456', '纪晓芙', 2, '7.jpg', 1, '2005-08-01', 1, now(), now()),
(8, 'zhouzhiruo', '123456', '周芷若', 2, '8.jpg', 1, '2014-11-09', 1, now(), now()),
(9, 'dingminjun', '123456', '丁敏君', 2, '9.jpg', 1, '2011-03-11', 1, now(), now()),
(10, 'zhaomin', '123456', '赵敏', 2, '10.jpg', 1, '2013-09-05', 1, now(), now()),
(11, 'luzhangke', '123456', '鹿杖客', 1, '11.jpg', 5, '2007-02-01', 3, now(), now()),
(12, 'hebiweng', '123456', '鹤笔翁', 1, '12.jpg', 5, '2008-08-18', 3, now(), now()),
(13, 'fangdongbai', '123456', '方东白', 1, '13.jpg', 5, '2012-11-01', 3, now(), now()),
(14, 'zhangsanfeng', '123456', '张三丰', 1, '14.jpg', 2, '2002-08-01', 2, now(), now()),
(15, 'yulianzhou', '123456', '俞莲舟', 1, '15.jpg', 2, '2011-05-01', 2, now(), now()),
(16, 'songyuanqiao', '123456', '宋远桥', 1, '16.jpg', 2, '2010-01-01', 2, now(), now()),
(17, 'chenyouliang', '123456', '陈友谅', 1, '17.jpg', NULL, '2015-03-21', NULL, now(), now());

创建一个新的 springboot 工程,选择引入对应的起步依赖:

mybatis4.png

application.properties 引入数据库连接信息

#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/db_mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=1234

创建对应的实体类 Emp

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Emp {
private Integer id;
private String username;
private String password;
private String name;
private Short gender;
private String image;
private Short job;
private LocalDate entrydate; //LocalDate类型对应数据表中的date类型
private Integer deptId;
private LocalDateTime createTime;//LocalDateTime类型对应数据表中的datetime类型
private LocalDateTime updateTime;
}

准备 Mapper 接口

/*@Mapper注解:表示当前接口为mybatis中的Mapper接口
程序运行时会自动创建接口的实现类对象(代理对象),并交给Spring的IOC容器管理
*/
@Mapper
public interface EmpMapper {
}

完成上述操作后,项目工程结构目录如下

mybatis5.png

6.2 功能实现

6.2.1 删除

6.2.1.1 删除功能

功能:根据主键删除数据

SQL 语句:

-- 删除id=17的数据
delete from emp where id = 17;

接口方法:

@Mapper
public interface EmpMapper {
/*
* 根据id删除数据
* */
@Delete("delete from emp where id=#{id}")//使用#{key}方式获取方法中的参数值
public void delete(Integer id);
}

Delete 注解:用于编写 delete 操作的 SQL 语句。如果 mapper 接口方法形参只有一个普通类型的参数,#{...} 里面的属性名可以随便写,但是建议保持名字一致。

测试:

@SpringBootTest
class MybatisMdApplicationTests {
@Autowired
private EmpMapper empMapper;
@Test
public void testDel(){
// 调用删除方法
empMapper.delete(16);
}
}

6.2.1.2 日志输入

在 Mybatis 当中我们可以借助日志,查看 sql 语句的执行、执行传递的参数以及执行结果。具体操作如下:

  1. 打开 application.properties 文件
  2. 开启 mybatis 的日志,并指定输出到控制台
# 指定mybatis输出日志的位置,输出到控制台
mybatis.configuration.logImpl=org.apache.ibatis.logging.stdout.StdOutImpl

开启日志之后,我们再次运行单元测试,可以看到在控制台中,输出了以下的 SQL 语句信息:

mybatis6.png

但是我们发现输出的 SQL 语句:delete from emp where id = ?,我们输入的参数 16 并没有在后面拼接,id 的值是使用?进行占位。那这种 SQL 语句我们称为预编译 SQL。

6.2.1.3 预编译 SQL

SQL 注入

SQL 注入:是通过操作输入的数据来修改事先定义好的 SQL 语句,以达到执行代码对服务器进行攻击的方法。

mybatis7.png

用户在页面提交数据的时候人为的添加一些特殊字符,使得 sql 语句的结构发生了变化,最终可以在没有用户名或者密码的情况下进行登录。

参数占位符

在 Mybatis 中提供的参数占位符有两种:${...} 和 #{...}。

#{...}:

  • 执行 SQL 时,会将 #{…} 替换为?,生成预编译 SQL,会自动设置参数值
  • 使用时机:参数传递,都使用 #{…}

${...}:

  • 拼接 SQL。直接将参数拼接在 SQL 语句中,存在 SQL 注入问题
  • 使用时机:如果对表名、列表进行动态设置时使用

6.2.2 新增

6.2.2.1 基本新增

SQL 语句:

insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values ('songyuanqiao','宋远桥',1,'1.jpg',2,'2012-10-09',2,'2022-10-01 10:00:00','2022-10-01 10:00:00');

接口方法:

@Insert("insert into emp (username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values " +
"(#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime});")
public void insert(Emp emp);

测试类:

@SpringBootTest
class MybatisMdApplicationTests {
@Autowired
private EmpMapper empMapper;
@Test
public void testInsert(){
Emp emp = new Emp();
emp.setUsername("tom");
emp.setName("汤姆");
emp.setImage("1.jpg");
emp.setGender((short)1);
emp.setJob((short)1);
emp.setEntrydate(LocalDate.of(2000,1,1));
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
emp.setDeptId(1);
//调用添加方法
empMapper.insert(emp);
}
}

日志输出:

mybatis8.png

6.2.2.2 主键返回

概念:在数据添加成功后,需要获取插入数据库数据的主键。

那要如何实现在插入数据之后返回所插入行的主键值呢?

默认情况下,执行插入操作时,是不会主键值返回的。如果我们想要拿到主键值,需要在 Mapper 接口中的方法上添加一个 Options 注解,并在注解中指定属性 useGeneratedKeys=true keyProperty="实体类属性名"

主键返回代码实现:

@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into emp (username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values " +
"(#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime});")
public void insert(Emp emp);

测试代码:

@SpringBootTest
class MybatisMdApplicationTests {
@Autowired
private EmpMapper empMapper;
@Test
public void testInsert(){
Emp emp = new Emp();
emp.setUsername("jack");
emp.setName("杰克");
emp.setImage("1.jpg");
emp.setGender((short)1);
emp.setJob((short)1);
emp.setEntrydate(LocalDate.of(2000,1,1));
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
emp.setDeptId(1);
//调用添加方法
empMapper.insert(emp);
System.out.println(emp.getId());
}
}

mybatis9.png

6.2.3 更新

SQL 语句:

update emp set username = 'linghushaoxia', name = '令狐少侠', gender = 1 , image = '1.jpg' , job = 2, entrydate = '2012-01-01', dept_id = 2, update_time = '2022-10-01 12:12:12' where id = 18;

接口方法:

/**
* 根据id修改员工信息
* @param emp
*/
@Update("update emp set username=#{username}, name=#{name}, gender=#{gender}, image=#{image}, job=#{job}, entrydate=#{entrydate}, dept_id=#{deptId}, update_time=#{updateTime} where id=#{id}")
public void update(Emp emp);

测试类:

@SpringBootTest
class MybatisMdApplicationTests {
@Autowired
private EmpMapper empMapper;
@Test
public void testUpdate(){
//要修改的员工信息
Emp emp = new Emp();
emp.setId(23);
emp.setUsername("songdaxia");
emp.setPassword(null);
emp.setName("老宋");
emp.setImage("2.jpg");
emp.setGender((short)1);
emp.setJob((short)2);
emp.setEntrydate(LocalDate.of(2012,1,1));
emp.setCreateTime(null);
emp.setUpdateTime(LocalDateTime.now());
emp.setDeptId(2);
//调用方法,修改员工数据
empMapper.update(emp);
}
}

6.2.4 查询

6.2.4.1 根据 ID 查询

SQL 语句:

select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp;

接口方法:

@Mapper
public interface EmpMapper {
@Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp where id=#{id}")
public Emp getById(Integer id);
}

测试类:

@SpringBootTest
class MybatisMdApplicationTests {
@Autowired
private EmpMapper empMapper;
@Test
public void testGetById(){
Emp emp = empMapper.getById(1);
System.out.println(emp);
}
}

执行结果:

mybatis10.jpg

可以看到 deptId, createTime 和 updateTime 字段为空,没有数值。

6.2.4.2 数据封装

出现上述问题的原因是,实体类名和数据库字段名不一致。实体类属性名和数据库表查询返回的字段名一致,mybatis 会自动封装。如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。

mybatis11.png

怎么解决这个问题呢?有以下三种方案:

  1. 起别名
  2. 结果映射
  3. 开启驼峰命名

起别名:在 SQL 语句中,对不一样的列名起别名,别名与实体类属性名一致

@Select("select id, username, password, name, gender, image, job, entrydate, " +
"dept_id AS deptId, create_time AS createTime, update_time AS updateTime " +
"from emp " +
"where id=#{id}")
public Emp getById(Integer id);

手动结果映射:通过 @Results@Result 进行手动结果映射

@Results({@Result(column = "dept_id", property = "deptId"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime")})
@Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp where id=#{id}")
public Emp getById(Integer id);

开启驼峰命名(推荐):如果字段名与属性名符合驼峰命名规则,mybatis 会自动通过驼峰命名规则映射。

驼峰命名规则:abc_xyz => abcXyz

表中字段名:abc_xyz

类中属性名:abcXyz

# 在application.properties中添加:
mybatis.configuration.map-underscore-to-camel-case=true

6.2.4.3 条件查询

任务要求:

  • 姓名:要求支持模糊匹配
  • 性别:要求精确匹配
  • 入职时间:要求进行范围查询
  • 根据最后修改时间进行降序排序

SQL 语句:

select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time
from emp
where name like '%张%'
and gender = 1
and entrydate between '2010-01-01' and '2020-01-01'
order by update_time desc;

接口方法:

  • 方式一

    @Select("select * from emp where name like '%${name}%' and gender = #{gender} and entrydate between #{begin} and #{end} order by update_time desc")
    public List list(@Param("name") String name, @Param("gender")Short gender, @Param("begin")LocalDate begin, @Param("end")LocalDate end);

    测试类:

    @SpringBootTest
    class MybatisMdApplicationTests {
    @Autowired
    private EmpMapper empMapper;
    @Test
    public void testList(){
    LocalDate begin = LocalDate.of(2010, 1, 1);
    LocalDate end = LocalDate.of(2020, 1, 1);
    Short gender = (short) 1;
    String name = "张";
    List list = empMapper.list(name, gender, begin, end);
    System.out.println(list);
    }
    }

    mybatis12.png

    模糊查询使用 ${...} 进行字符串拼接,这种方式呢,由于是字符串拼接,并不是预编译的形式,所以效率不高、且存在 sql 注入风险。

  • 方式二(解决 SQL 注入风险)

    使用 MySQL 提供的字符串拼接函数:concat ('%' , ' 关键字 ' , '%')

    @Select("select * from emp where name like concat('%', #{name}, '%') and gender = #{gender} and entrydate between #{begin} and #{end} order by update_time desc")
    public List list(@Param("name") String name, @Param("gender")Short gender, @Param("begin")LocalDate begin, @Param("end")LocalDate end);

    mybatis13.png

6.3 Mybatis 的 XML 配置文件

6.3.1 XML 配置文件规范

上述案例中我们都是通过注解的方式来实现一些简单的功能,如果需要实现复杂的 SQL 功能,建议使用 XML 来配置映射语句,也就是将 SQL 语句写在 XML 配置文件中。

在 Mybatis 中使用 XML 映射文件方式开发,需要符合一定的规范:

  1. XML 映射文件的名称 Mapper 接口名称一致,并且将 XML 映射文件和 Mapper 接口防止在相同包下(同包同名)
  2. XML 映射文件的 namespace 属性为 Mapper 接口全限定名一致
  3. XML 映射文件中 sql 语句 id 与 Mapper 接口中的方法名一致,并保持返回类型一致。

首先是,第一点同包同名:

mybatis14.png

然后是第二点、第三点:

mybatis15.png

\


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇