FutureWL's Blog

我的程序之旅


  • 首页

  • 不周山

  • 通天塔

  • 好望角

  • 书影音

  • 标签

  • 分类

  • 归档

Docker开启远程端口

发表于 2019-06-27

进入到/lib/systemd/system/docker.service

1
vim /lib/systemd/system/docker.service

找到ExecStart行,修改成下边这样

1
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

配置生效

1
docker daemon-reload

重启docker

1
systemctl restart docker

基于Docker的Mysql主从复制搭建

发表于 2019-06-25

首先拉取docker镜像,我们这里使用5.7版本的mysql:

docker pull mysql:5.7

然后使用此镜像启动容器,这里需要分别启动主从两个容器

Master(主):

1
docker run -p 3306:3306 --name mysql-master -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7

Slave(从):

1
docker run -p 3307:3306 --name mysql-slave --link mysql-master:mysql-master -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7

配置Master(主)

通过

1
docker exec -it 627a2368c865 /bin/bash

命令进入到Master容器内部,也可以通过

1
docker exec -it mysql-master /bin/bash

命令进入。627a2368c865 是容器的id,而mysql-master是容器的名称。

1
cd /etc/mysql

切换到/etc/mysql目录下,然后vi my.cnf对my.cnf进行编辑。此时会报出bash: vi: command not found,需要我们在docker容器内部自行安装vim。

使用

1
apt-get install vim

命令安装vim

会出现如下问题:

1
2
3
4
Reading package lists... Done
Building dependency tree
Reading state information... Done
E: Unable to locate package vim

执行 apt-get update,然后再次执行apt-get install vim即可成功安装vim。然后我们就可以使用vim编辑my.cnf,在my.cnf中添加如下配置:

1
2
3
4
5
[mysqld]
## 同一局域网内注意要唯一
server-id=100
## 开启二进制日志功能,可以随便取(关键)
log-bin=mysql-bin

配置完成之后,需要重启mysql服务使配置生效。

使用

1
service mysql restart

完成重启。

重启mysql服务时会使得docker容器停止,

我们还需要

1
docker start mysql-master

启动容器。

下一步在Master数据库创建数据同步用户,授予用户 slave REPLICATION SLAVE 权限 和 REPLICATION CLIENT 权限,用于在主从库之间同步数据。

1
2
3
CREATE USER 'slave'@'%' IDENTIFIED BY '123456';

GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'slave'@'%';

配置Slave(从)

和配置Master(主)一样,在Slave配置文件my.cnf中添加如下配置:

1
2
3
4
5
6
7
[mysqld]
## 设置server_id,注意要唯一
server-id=101
## 开启二进制日志功能,以备Slave作为其它Slave的Master时使用
log-bin=mysql-slave-bin
## relay_log配置中继日志
relay_log=edu-mysql-relay-bin

配置完成后也需要重启mysql服务和docker容器,操作和配置Master(主)一致。

链接Master(主)和Slave(从)

在Master进入mysql,执行

1
show master status;

File和Position字段的值后面将会用到,在后面的操作完成之前,需要保证Master库不能做任何操作,否则将会引起状态变化,File和Position字段的值变化。

在Slave中进入 mysql,执行

1
change master to master_host='mysql-master', master_user='slave', master_password='123456', master_port=3306, master_log_file='mysql-bin.000001', master_log_pos= 2830, master_connect_retry=30;

在Slave 中的mysql终端执行show slave status \G;用于查看主从同步状态。

正常情况下,SlaveIORunning 和 SlaveSQLRunning 都是No,因为我们还没有开启主从复制过程。使用start slave开启主从复制过程,然后再次查询主从同步状态show slave status \G;。

SlaveIORunning 和 SlaveSQLRunning 都是Yes,说明主从复制已经开启。此时可以测试数据同步是否成功。

使用start slave开启主从复制过程后,如果SlaveIORunning一直是Connecting,则说明主从复制一直处于连接状态,这种情况一般是下面几种原因造成的,我们可以根据 Last_IO_Error提示予以排除。

  1. 网络不通 –> 检查ip,端口
  2. 密码不对 –> 检查是否创建用于同步的用户和用户密码是否正确
  3. pos不对 –> 检查Master的 Position

测试主从复制

测试主从复制方式就十分多了,最简单的是在Master创建一个数据库,然后检查Slave是否存在此数据库。

Spring Data Rest 快速上手(一)

发表于 2019-04-22

Spring Data Rest 是基于 Spring Data repositories,分析实体之间的关系。为我们生成Hypermedia API(HATEOAS)风格的Http Restful API接口。
Spring Data Rest 官方首页中提到了它所具有的特性,比如:

根据model,生成HAL风格的restful API
根据model,维护实体之间的关系
支持分页
…

诸多的特性,官方文档都会有提及。这里我们着重关注在Spring Data Rest中基于JPA维护实体之间关系。
资源实体的关系

实体关系E-R图

  • 一个用户(user)拥有一个身份证(card)
  • 一个用户(user)拥有多辆车(car)
  • 一个用户(user)拥有多门语音(language)
  • 一门语言(language)拥有多个用户(user)

关系不用在意是否合理,只是为了涵盖几个基本的关系

one to one 关系

数据实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@Entity
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

private Integer age;

private Date createAt;

@OneToOne(mappedBy = "user")
private Card card;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
@Entity
public class Card {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String card_num;

private Date createAt;

@OneToOne
@JoinColumn(name = "user_id")
@RestResource(path = "user", rel = "user")
private User user;

}

Repository结构

1
2
public interface UserRepository extends JpaRepository<User, Long> {
}
1
2
public interface CardRepository extends JpaRepository<Card, Long> {
}

通过以上的代码,Spring Data Rest 就已经足够帮我们维护其用户(user)和身份证(card)二者的关系,并且提供了HAL的接口。就是这么方便!下面,我们使用HAL Browser ,可以更加方便的在浏览器中查看接口,以及他们之间的关系。
HAL Browser 使用
HAL-browser 是基于hal+json的media type的API浏览器,Spring Data Rest 提供了集成,pom文件中加个这个。

1
2
3
4
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>

启动我们的程序,打开浏览器 http://127.0.0.1:8081/api/v1/browser/index.html#/api/v1

api/v1 是我指定的接口前缀,通过配置项 spring.data.rest.base-path 指定

可以看到如下界面

hal-browser

具体的使用这里不再赘述,可以自己点点或者看看这个。
接口调用
新增一个user

1
curl -i -X POST -H "Content-Type:application/json" -d "{\"name\":\"Lucy\",\"age\":25}" http://127.0.0.1:8081/api/v1/users

结果返回:

新增用户

返回结果的状态码是201
返回实体内容
返回_links资源,分别指向自己和对应的card资源的URI
新增一个card

1
curl -i -X POST -H "Content-Type:application/json" -d "{\"cardNum\":\"num1\"}" http://127.0.0.1:8081/api/v1/cards

新增card

建立关系
创建的两个实体之后,我们需要建立起二者的关系。在Spring Data Rest中,二者的关系绑定,是通过URI来维护,用PUT请求动作。

1
curl -i -X PUT -H "Content-Type:text/uri-list" -d "http://127.0.0.1:8081/api/v1/users/1" http://127.0.0.1:8081/api/v1/cards/1/user

也可以用这样维护

1
curl -i -X PUT -H "Content-Type:text/uri-list" -d "1" http://127.0.0.1:8081/api/v1/cards/1/user

或者:

1
curl -i -X PUT -H "Content-Type:text/uri-list" -d "api/v1/users/1" http://127.0.0.1:8081/api/v1/cards/1/user

三者是等价的操作。
如果创建成功,将会返回响应码204,如下图:

关系创建

我们来核实下user下的card

1
curl -i -X GET http://127.0.0.1:8081/api/v1/users/1/card

返回如下:

获取user的card

上图可以看出,user-card的关系维护成功!

以上是通过PUT Card资源的User来维护二者关系。外键在于Card上,是资源维护方。反过来就通过User资源的Card维护是不被允许的。这个是我的一个疑问,没有深入研究过,一个tip

one to many 关系
数据实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
@Entity
public class Car {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String carNum;

private Date createAt;

@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}

@Data
@Entity
public class User {

// ...

@OneToMany(mappedBy = "user")
private List<Car> cars = new ArrayList<>();

// …
}

1
Repository结构

public interface CarRepository extends JpaRepository<Car, Long> {
}

1
2
3

接口调用
新增一个car

curl -i -X POST -H “Content-Type:application/json” -d “{"carNum":"A1001"}” http://127.0.0.1:8081/api/v1/cars

1
2
3
4
5
6
7
8
9
10
返回如下:







建立关系
一对多的关系中,也是通过URI通过PUT请求维护关系。

curl -i -X PUT -H “Content-Type:text/uri-list” -d “http://127.0.0.1:8081/api/v1/users/1" http://127.0.0.1:8081/api/v1/cars/1/user

1
2
3
4
5
6
7
8
9
结果返回:







我们看下User下的Cars

curl -i -X GET http://127.0.0.1:8081/api/v1/users/1/cars

1
2
3
4
5
6
7
8
9
10
11
12
13
14
结果如下:







很明显,已经可以看到User下的Cars。

也是只有通过多的一方维护关系

many to many 关系
数据实体

@Data
@Entity
public class Language {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

@ManyToMany
@JoinTable(name = "rel_user_language",
        joinColumns = @JoinColumn(name = "language_id", referencedColumnName = "id", nullable = false),
        inverseJoinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false))
private List<User> users = new ArrayList<>();

}

1
2
3
4
5
6
7
8
9
10
11
```
@Data
@Entity
public class User {
// ...

@ManyToMany(mappedBy = "users", cascade = CascadeType.ALL)
private List<Language> languages = new ArrayList<>();

//...
}

Repository结构

1
2
public interface LanguageRepository extends JpaRepository<Language, Long> {
}

接口调用
同样新增方式,多门语言,多个用户

1
curl -i -X POST -H "Content-Type:application/json" -d "{\"name\":\"eng\"}" http://127.0.0.1:8081/api/v1/languages
1
curl -i -X POST -H "Content-Type:application/json" -d "{\"name\":\"chs\"}" http://127.0.0.1:8081/api/v1/languages
1
curl -i -X POST -H "Content-Type:application/json" -d "{\"name\":\"jp\"}" http://127.0.0.1:8081/api/v1/languages
1
curl -i -X POST -H "Content-Type:application/json" -d "{\"name\":\"Jack\",\"age\":25}" http://127.0.0.1:8081/api/v1/users

创建关系
多对多的关系创建方式比之前两种更为丰富。
PUT方式添加关系

1
curl -i -X PUT -H "Content-Type:text/uri-list" -d "api/v1/users/1" http://127.0.0.1:8081/api/v1/languages/1/users

POST方式添加关系

1
curl -i -X POST -H "Content-Type:text/uri-list" -d "api/v1/users/1" http://127.0.0.1:8081/api/v1/languages/2/usersrs

PATCH方式添加关系

1
curl -i -X PATCH -H "Content-Type:text/uri-list" -d "api/v1/users/1" http://127.0.0.1:8081/api/v1/languages/3/users

以上三种方式都可以用于创建多对多的关系,可以查看下:

1
curl -i -X GET http://127.0.0.1:8081/api/v1/users/1/languages
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
"_embedded" : {
"languages" : [ {
"name" : "eng",
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/1"
},
"language" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/1"
},
"users" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/1/users"
}
}
}, {
"name" : "chs",
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/2"
},
"language" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/2"
},
"users" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/2/users"
}
}
}, {
"name" : "jp",
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/3"
},
"language" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/3"
},
"users" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/3/users"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/languages"
}
}
}

结果上是可以看出,三种方式的结果都成功了。

反过来,通过语言查看用户

1
curl -i -X GET http://127.0.0.1:8081/api/v1/languages/3/users
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"_embedded" : {
"users" : [ {
"name" : "Lucy",
"age" : 25,
"createAt" : null,
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1"
},
"user" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1"
},
"card" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/card"
},
"languages" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/languages"
},
"cars" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/cars"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/3/users"
}
}
}

结果也是我们所想要的。
PUT POST PATCH 三者之间区别
PATCH 请求的作用等同于 POST 请求,而他们二者有点不同于PUT请求。大家都知道 PUT 请求是整体替换,而PATCH是局部更新。在Spring Data Rest 中 PATCH 表示添加,而不是覆盖,PUT请求是完全覆盖。

我们在原来的数据基础上给User2(Jack)添加一门Language:

1
curl -i -X PATCH -H "Content-Type:text/uri-list" -d "api/v1/users/2" http://127.0.0.1:8081/api/v1/languages/1/users

之后,我们查看下 id 为1的 language 的users

1
curl -i -X GET http://127.0.0.1:8081/api/v1/languages/1/users
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
{
"_embedded" : {
"users" : [ {
"name" : "Lucy",
"age" : 25,
"createAt" : null,
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1"
},
"user" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1"
},
"card" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/card"
},
"languages" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/languages"
},
"cars" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/cars"
}
}
}, {
"name" : "Jack",
"age" : 25,
"createAt" : null,
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2"
},
"user" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2"
},
"card" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2/card"
},
"languages" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2/languages"
},
"cars" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2/cars"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/1/users"
}
}
}

可以看到拥有两个user。这里可以看出 PATCH 的作用添加了一个item。
接下来,我们再调用 PUT 请求,更新下。

1
curl -i -X PUT -H "Content-Type:text/uri-list" -d "api/v1/users/2" http://127.0.0.1:8081/api/v1/languages/1/users

与上一个请求的唯一区别是用了PUT做更新

再GET下User,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{                                                                                        
"_embedded" : {
"users" : [ {
"name" : "Jack",
"age" : 25,
"createAt" : null,
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2"
},
"user" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2"
},
"card" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2/card"
},
"languages" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2/languages"
},
"cars" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2/cars"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/1/users"
}
}
}

对比很明显,PUT请求覆盖了之前的数据,只留下了一个Jack的user关联。这就是PUT 和 PATCH 的区别。

tip1:POST的效果同PATCH 不做多说明。

tip2:多对多的关系维护中,维护方的资源来维护二者关系。

Spring Data Rest Events
Spring Data Rest Events 提供了AOP方式的开发,定义了10种不同事件。

资源保存前 @HandleBeforeCreate
资源保存后 @HandleAfterCreate
资源更新前 @HandleBeforeSave
资源更新后 @HandleAfterSave
资源删除前 @HandleBeforeDelete
资源删除后 @HandleAfterDelete
关系创建前 @HandleBeforeLinkSave
关系创建后 @HandleAfterLinkSave
关系删除前 @HandleBeforeLinkDelete
关系删除后 @HandleAfterLinkDelete

不同的事件触发的场景不同,我们可以自定义这些事件内容来完成我们的业务。这个以后再说…有兴趣可以看看我的样例代码

Java基础教程-01-Java语言概述

发表于 2019-03-18 | 分类于 编程语言

Java语言历时30多年,已发展成为人类计算机史上影响深远的编程语言,从某种程度上来看,它甚至超出了编程语言的范畴,成为一种开发平台,一种开发规范。更甚至于:Java 已成为一种信仰,Java语言所崇尚的开源、自由等精神,吸引了全世界无数优秀的程序员。是事实,从计算机诞生以来从没有一门编程语言能吸引这么多的程序员,也没有一门编程语言能衍生出如此多的开源框架。

Java 语言是一门非常纯粹的面向对象编程语言,它吸收了 C++ 语言的各种优点,又摒弃了 C++ 里难以理解的多继承、指针等概念,因此 Java 语言具有功能强大和简单易用两个特征。Java 语言作对静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程开发。

不仅如此,Java 语言相关的 Java EE 规范里包含了时下最流行的各种软件工程理念,各种先进的设计思想总能在 Java EE 规范、平台以及相关框架里找到相应实现。从某种程度上看,学精了 Java 语言的相关方面,相当于系统地学习了软件开发的相关知识,而不仅仅学完了一门编程语言。

1.1 Java语言的发展简史

  • 1990 Sun 公司计划开发 “Green计划” 准备为智能家电提供通用控制系统
  • 1992 Green 计划 转为 “FirstPerson 有限公司”
  • 1994 Oak 更名 Java
  • 1995 Sun 公司发布 Java 语言 并公开源代码
  • 1996 发布 JDK 1.0
  • 1998 发布 JDK 1.2 (最重要的里程碑版本)
    • J2ME: 控制移动设备和家电(嵌入式设备)已经凉凉
    • J2SE: 整个 Java 技术的核心和基础,它是 J2EE 和 J2ME 编程的基础
    • J2EE: 整个 Java 中应用最广泛的部分,提供了企业应用开发的完整解决方案
  • 2002 发布 JDK 1.4(众多公司参与,发展最快的 JDK版本)
  • 2004 发布 JDK 1.5
  • 2006 发布 JDK 1.6
  • 2009 Oracle 收购 Sun
  • 2011 Oracle 发布 JDK 1.7
  • 2014 Oracle 发布 JDK 1.8
  • 2017 Oracle 发布 JDK 1.9

1.2 Java语言的运行机制

1.2.1 高级语言的运行机制

1.2.2 Java程序的运行机制和JVM

从零开始 搭建内网穿透服务器

发表于 2018-12-14

所需原料

  1. Linux 系统服务器(可公网访问并开放80端口)
  2. Go 语言环境
  3. Ngrok 1.x 源码 (已停止维护)
  4. 域名一枚(备案或非备案均可)

所需步骤

  1. 准备一台 Linux 系统的服务器

  2. 安装相关依赖

1
2
3
# 以 Ubuntu 为例

# apt-get install golang
  1. 克隆 Ngork 1.x 源码
1
2
3
# git clone https://www.github.com/futurewl/ngrok
# make
# ./bin/ngrokd
  1. 编译

  2. 启动 Ngrok 服务

  3. 将域名解析到此服务器

  4. 启动内网应用

  5. 准备配置文件

  6. 启动内网 ngrok 客户端

  7. 实现内网穿透

第五章 Spring Data JPA 高级

发表于 2018-08-23 | 分类于 轻松愉快之玩转SpringData

CrudRepository 接口使用详解

  • save(entity)
  • save(entities)
  • findOne(id)
  • findAll()
  • exists(id)
  • delete(entity)
  • delete(id)
  • delete(entities)
  • deleteAll()

PagingAndSortingRepository 接口使用详解

  • findAll(Sort sort)
  • finaAll(Pageable pageable)

JpaRepository 接口使用详解

JpaSpecificationExecutor 接口使用详解

第四章 Spring Data JPA 进阶

发表于 2018-08-23 | 分类于 轻松愉快之玩转SpringData

关于 Repository 接口

Repository 讲解

  • Repository 接口是 Spring Data 的核心接口,不提供任何方法
  • public interface Repository<T,ID extends Serializable> { }
  • @RepositoryDefinition 注解的使用

Repository是一个空接口,标记接口 没有包含方法声明的接口

阅读全文 »

第三章 Spring Data 快速起步

发表于 2018-08-23 | 分类于 轻松愉快之玩转SpringData

开发环境搭建

引入 Maven 依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- Spring data jpa -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.8.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.3.6.Final</version>
</dependency>
阅读全文 »

第二章 使用传统方式访问数据库

发表于 2018-08-22 | 分类于 轻松愉快之玩转SpringData

课程目录

传统方式访问数据库

  • JDBC
  • Spring JdbcTemplate
  • 弊端分析
阅读全文 »

第一章 课程介绍

发表于 2018-08-22 | 分类于 轻松愉快之玩转SpringData

Spring Data 概览

什么是 Spring Data

主旨:provide a familiar and consistent,Spring-based programming model for data access.

历史:2010,作者 Rod Johnso,Spring Source 项目

网址:http://projects.spring.io/spring-data/#quick-start

阅读全文 »
12…4
FutureWL

FutureWL

想要成为全栈的小猿

31 日志
4 分类
15 标签
GitHub E-Mail
Links
  • 蓝色技术工作室
© 2015 — 2019 FutureWL