引子

最近入职了新公司一段时间了,做了一个项目。 用到很多之前了解过,但一直没机会实战的技术。接下来用几篇文章来总结一下使用到的技术和遇到的一些问题。

目前会包括 2 个部分

  • gitLab-ci 自动构建
  • k8s 自动发布集群

整体的发布流程

接下来就先从 gitlab-ci 开始

GitLab-CI

基本条件

  • 拥有 gitLab 官方账号,或者有私有的 gitLab 仓库。
  • 会使用 GIT
  • 如果以上都不会,那面壁去吧。
  • 最后先熟悉一下 官方文档,虽然不是必须,但是有助于接下来的操作。

构建流程

步骤

  • 首先开开心心的去 gitLab 创建一个新项目。以我的为例 http://gitlab.**.com/skylee/my_test_ci
  • 在服务器上安装 gitLab-runner 服务
  • 注册 runner
  • 在项目根目录下新增文件 .gitlab-ci.yml

Usage

把代码打包成镜像并推送到远程仓库

项目代码 只有一个 index.php 文件

1
2
3
4
my_test_ci git:(master) tree
.
├── Dockerfile
└── index.php

其中 Dockerfile 文件为构建镜像,主要作用就是把 index.php 打包进镜像中。Dockerfile 的内容

1
2
3
4
5
FROM php:7.2-fpm
VOLUME /data
ADD . /data/www
WORKDIR /data/www
EXPOSE 9000

配置 .gitlab-ci.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
image: docker.io/skylee:docker

stages:
- package

docker-build:
stage: package
before_script:
- source /etc/profile.d/docker
script:

- echo 'Building...'
- docker login -u $REPOSITORY_USER -p $REPOSITORY_PASSWORD hub.docker.com
- docker build -t skylee/phptest:latest .
- docker push skylee/phptest:latest

docker.io/skylee:docker 是基于 docker:git 镜像,添加了一些环境变量。

git push 之后会触发构建过程
1
2
3
4
5
6
7
8
9
10
Running with gitlab-runner 10.8.0 (079aad9e)
on 192-168-53-35 337b5bc7
Using Docker executor with image docker.io/skylee:docker ...
Pulling docker image docker.io/skylee:docker ...
Using docker image sha256:58cf969aeca7f67649540fb60b1853a153dc3660bbb2e0b7ba33cdb752723cc7 for docker.io/skylee:docker ...
Running on runner-337b5bc7-project-34-concurrent-0 via 192-168-53-35...
Cloning repository...

Cloning into '/builds/likai15/my_test_ci'...

...
  • 使用 image: docker.io/skylee:docker 启动一个容器。之后所有的命令都是在容器内操作的。
  • 启动 docker:dind 服务。也就是 docker 的进程。
  • gitlab-runner 克隆代码到容器目录 /builds/likai15/my_test_ci。 由于没有指定挂载目录,可以在宿主机的默认挂载目录看到 /var/lib/docker/xxxx/_data/my_test_ci
  • 开始执行 docker-build 阶段的 job。

FAQ

  • mount: permission denied (are you root?) 答: 这里

  • docker:dind 是啥东西? 答 :这里

  • 构建过程耗时过长 ? 答:一般我们都会自己搭建一个私有的内网仓库。

搭建私有 docker 仓库

1
docker run -d -p 5000:5000 -v /registry:/tmp/registry registry

只是简单搭建了一个用 docker.images 的私有仓库。也可以使用 [Harbor] 。(https://github.com/vmware/harbor/)
参考 搭建 Harbor

Sqoop

安装

  • 下载 sqoop-1.4.6.bin__hadoop-2.0.4-alpha.tar.gz 网址
  • 配置环境变量
1
2
3
export HADOOP_COMMON_HOME=/home/hadoop/opt/hadoop-2.6.0

export HADOOP_MAPRED_HOME=/home/hadoop/opt/hadoop-2.6.0
  • 下载 mysql jra包 地址 把 mysql-connector-java-5.1.45-bin.jar 复制到 SQOOP_HOME/lib

一些例子:

把 mysql 数据导入到 hdfs
1
2
3
4
5
6
7
8
9
10
11
# 链接数据库  这个只做提示作用,相当于是个变量
$c = " --connect jdbc:mysql://192.168.0.89:3306/tob --username web --password passwd "

# 查看数据库列表
sqoop list-databases $c

-- 把表 tob_dealer 数据导出到 /tmp 目录, 为 hadoop 文件系统目录
bin/sqoop import $c --table tob_dealer --target-dir /tmp

-- 把 tob_dealer 表中的 id,link_phone 字段导入到 hdfs 的 /tmp 目录下
bin/sqoop import $c --table tob_dealer --columns "id,link_phone" -m 1 --target-dir /tmp
把 mysql 数据导入到 Hive 数据仓库表
1
2
3
4
5
6
7
8
9
10
11
12
// -m 1 表示 map/reduce 作业的个数
//不指定表名称 会自动创建一张同名表(tob_dealer)来保存数据
bin/sqoop import --hive-import $c --table tob_dealer -m 1

//指定表名称 hive_tob_dealer。 如果不存在,hive 会自动创建
bin/sqoop import --hive-import $c --table tob_dealer -m 1 --hive-table hive_tob_dealer

//指定 Where 条件。 只导id=1的得数据
bin/sqoop import --hive-import $c --table tob_dealer -m 1 --hive-table hive_tob_dealer --where 'id=1'

//指定 Select 条件。 只导id=1的得数据, 不需要指定 table。必须指定 and $CONDITIONS
bin/sqoop import --hive-import $c -m 1 --query 'select * from tob_dealer where id > 10 and $CONDITIONS' --target-dir "/tmp/select" --hive-table hive_tob_dealer
hive 导出到 Mysql
1
2
3
-- 把 /tmp 目录下的数据导出到 mysql 的表 tob_dealer 中
-- 需要先在 mysql 创建对应的表
bin/sqoop export $c --table tob_dealer --export-dir "/tmp"

sqoop 遇到的问题

  • Error: Could not find or load main class org.apache.sqoop.Sqoop 这是因为找不到sqoop-1.4.5.jar文件导致的。
1
2
3
4
5
修改 $SQOOP_HOME/bin/sqoop 脚本最后一行: 
exec ${HADOOP_COMMON_HOME}/bin/hadoop org.apache.sqoop.Sqoop "$@"
改为
exec ${HADOOP_COMMON_HOME}/bin/hadoopjar $SQOOP_HOME sqoop-1.4.5.jar org.apache.sqoop.Sqoop "$@"
即:显示指定sqoop-1.4.5.jar文件的所在位置。
  • 导数据的时候报错: Could not load org.apache.hadoop.hive.conf.HiveConf. Make sure HIVE_CONF_DIR is set correctly.
1
2
3
vi ~/.bashrc
export HADOOP_CLASSPATH=$HADOOP_CLASSPATH:/hive/lib/*
source ~/.bash_profile

参考列表

Hive

hive 的表就是 hdfs 的目录,数据就是 hdfs 的文件

嵌入模式:hive + derby,demo

每次执行 hive 命令,都会在当前目录中生成数据 derby 文件

在 console 管理界面执行命令 dfs ls

desc tableName

! 系统命令

Hive -S 静默模式

create table (id INT, name STRING)

安装客户端工具

Hadoop+Hive环境搭建

远程管理

1
2
3
4
5
6
7
启动web管理服务 hive --service hwi
编译war包:src/hwi jar cvfM0 hive-hwi-0.13.0.war -C web/ .
拷贝到hive的lib目录下
修改hive-site.xml
cp jdk/lib/tools.jar hive/lib/

hive --service hiveserver &

数据类型的一些例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 // 学生成绩
create table s (id int, name String, grade array<float>); -- {1, tom [90, 80.00]}

create table s2 (id int, name String, grade map<string, float>); -- {1, tom <"语文", 80.00>}

create table s3 (id int, name String, grade array<map<string, float>>); -- {1, tom [<"语文", 80.00>, <"英语", 90.00>]}

//结构数据类型
create table s4 (id int, info struct<name:string, age:int>); -- {1, {"tom", "19"}}

// 创建表并指定存储位置,不指定的情况下会默认在 /usr/hive/warehouse
create table s5(id int) location '/pwd/s5';

// 指定列的分割符号为 逗号
create table s6 (id int, name String) row format delimited fields terminated by ',';

//创建表并从 s5导入数据,以逗号为列的分隔符
crate table t7 row format delimited fields terminated by ',' as select * form s5;

select unix_timestamp();

date 数据类型只有日期,没有具体到几点的时间

分区表

1
2
3
4
//基于 gender 的分区表
create table partition_table(id int, name String) partitioned by (gender String) row format delimited fields terminated by ',';

insert into table partition_table partition(gender='M') slect id, name from sample_data where gender = 'M';

外部表

1
2
3
4
create external table external_student (sid int, sname string, age int) row format delimited fields terminated by ',' location '/input';

// 把本地文件导入到 hive 数据库中
LOAD DATA LOCAL INPATH '/data/login/20130101.csv' OVERWRITE INTO TABLE login PARTITION (dt='20130101');

桶表

1
2
create table bucket_table (sid int, sname string, age int) clustered by (sname) into 5 buckets row format delimited fields terminated by ',';
//创建一个桶表,这个桶表是以sname作为哈希运算,运算后的结果放到5个桶中

视图

1
2
3
4
5
6
create view empinfo
as
select e.empno,e.ename,e.sal,e.sal*12 annlsal, d.dname
from emp e, dept d
where e.deptho=d.deptho;
//e.sal*12 annlsal :为e.sal*12起一个别名叫annlsal. 其他操作和普通的表一样。

数据导入

1
2
3
//导本机目录下的数据到t3表,并覆盖原来的数据。

load data local inpath '/root/data/' overwrite into table t3;

FLume

核心概念

一个 flume 就是一个 agent。agent 由三部分组成。可以灵活配置

图片

下载安装

下载 bin 包,下载就可使用。绿色软件。

1
http://www.apache.org/dyn/closer.lua/flume/1.8.0/apache-flume-1.8.0-bin.tar.gz

配置 JAVA_HOME 环境变量。用 java 1.8 jdk。

快速开始 文档

完整文档 点击

参考

Troubleshooting

使用Flume拉取到HDFS中的文件格式错乱

这是因为HDFS Sink的配置中,hdfs.writeFormat属性默认为 “Writable” 会将原先的文件的内容序列化成HDFS的格式,应该手动设置成 hdfs.writeFormat=”text”; 并且 hdfs.fileType 默认是 “SequenceFile” 类型的,是将所有 event 拼成一行,应该该手动设置成 hdfs.fileType=”DataStream”,这样就可以是一行一个event,与原文件格式保持一致

所以啊,要多看文档啊。 有时候英文文档感觉描叙不到位。等我找时间翻译下这个文档。

后台启动

可以用 nohup bin/flume-ng agent -n agent -c conf -f conf/flume-conf.properties > /dev/null &
但是生产环境还是用 supervisor 来管理吧

实战1 配置本机的 HDFS.

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
# 命名
tcm.channels = ch-tcm
tcm.sources = src-tcm
tcm.sinks = sink-tcm
# 配置 agent 的 channels
tcm.channels.ch-tcm.type = file
tcm.channels.ch-tcm.checkpointDir = /channels/checkpoint
tcm.channels.ch-tcm.dataDirs = /channels/data

# 配置 agent 的 sources
tcm.sources.src-tcm.type = spooldir
tcm.sources.src-tcm.spoolDir = /home/wwwroot/hadoop/flume-1.8.0/input/sources

# 配置 sinks
tcm.sinks.sink-tcm.type = hdfs
tcm.sinks.sink-tcm.hdfs.path = /hadoop-2.9.0/flume/%y-%m-%d
# 使用本机时间配置
tcm.sinks.sink-tcm.hdfs.useLocalTimeStamp = true
tcm.sinks.sink-tcm.hdfs.inUsePrefix = .
# 保留源文件的格式
tcm.sinks.sink-tcm.hdfs.writeFormat = Text
# 配置成一行一个 event 认是“SequenceFile”类型的,是将所有event拼成一行,应该该手动设置成hdfs.fileType=“DataStream”,这样就可以是一行一个event,
与原文件格式保持一致
tcm.sinks.sink-tcm.hdfs.fileType = DataStream

# 通过 channel 链接 sources 和 sinks
tcm.sources.src-tcm.channels = ch-tcm
tcm.sinks.sink-tcm.channel = ch-tcm

启动

bin/flume-ng agent --conf conf/ -f conf/flume.conf -Dflume.root.logger=INFO,console -n tcm

实战2 应用服务器上的数据传到 hadoop 的 namenode 服务器

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
53
54
55
56
57
58
59
60
61
62
63
64
# namenode 配置 conf/33.10.conf
# 命名
tcm.channels = ch-tcm
tcm.sources = src-tcm
tcm.sinks = sink-tcm

# 配置 agent 的 channels
tcm.channels.ch-tcm.type = file
tcm.channels.ch-tcm.checkpointDir = /channels/checkpoint
tcm.channels.ch-tcm.dataDirs = /channels/data

# 配置 agent 的 sources
tcm.sources.src-tcm.type = avro
tcm.sources.src-tcm.bind = 0.0.0.0
tcm.sources.src-tcm.port = 4545


# 配置 sinks
tcm.sinks.sink-tcm.type = hdfs
tcm.sinks.sink-tcm.hdfs.path = /hadoop-2.9.0/flume/%y-%m-%d
# 使用本机时间配置
tcm.sinks.sink-tcm.hdfs.useLocalTimeStamp = true
tcm.sinks.sink-tcm.hdfs.inUsePrefix = .
# 保留源文件的格式
tcm.sinks.sink-tcm.hdfs.writeFormat = Text
# 配置成一行一个 event 认是“SequenceFile”类型的,是将所有event拼成一行,应该该手动设置成hdfs.fileType=“DataStream”,这样就可以是一行一个event,与原文件格式保持一致
tcm.sinks.sink-tcm.hdfs.fileType = DataStream

# 通过 channel 链接 sources 和 sinks
tcm.sources.src-tcm.channels = ch-tcm
tcm.sinks.sink-tcm.channel = ch-tcm

# 启动命令 bin/flume-ng agent -c conf/ -f conf/33.10.conf -Dflume.root.logger=INFO,console -n tcm

# ================================================================
# 应用服务器 192.168.33.10 conf/f2.conf
# 命名
agent.sources = src_test10
agent.channels = ch_test10
agent.sinks = sink_test10

# 配置 channel
agent.channels.ch_test10.type = memory
agent.channels.ch_test10.capacity = 10000

# 配置 source
agent.sources.src_test10.type = spooldir
agent.sources.src_test10.spoolDir = /tmp/flume
agent.sources.src_test10.deserializer.maxLineLength = 2048
agent.sources.src_test10.interceptors = i2
agent.sources.src_test10.interceptors.i2.type = host
agent.sources.src_test10.interceptors.i2.hostHeader = hostname

# 配置 sinks
agent.sinks.sink_test10.type = avro
agent.sinks.sink_test10.channel = ch_test10
agent.sinks.sink_test10.hostname = 192.168.33.33
agent.sinks.sink_test10.port = 4545

# chanel 链接 source 和 sink
agent.sources.src_test10.channels = ch_test10
agent.sinks.sink_test10.channel = ch_test10

## 启动 bin/flume-ng agent -c conf/ -f conf/f2.conf -Dflume.root.logger=INFO,console -n agent

Hadoop 集群安装

准备有2台主机, 登陆到2台(或者多台)服务器 修改 /etc/hosts,内容如下

多台服务器,可用 salt 相关命令批管理。

  • 192.168.33.33 namenode
  • 192.168.33.10 datanode

创建 Hadoop 用户组

  • groupadd hadoop
  • useradd -g hadoop hadoop

配置 namenode 免密登陆 datanode

  • ssh-keygen -t rsa 一路回车,生成公钥
  • .ssh/id_rsa.pub 的内容写入到 datanode 的 ~/.ssh/.ssh/authorized_keys
  • 修改 datanode 目录权限 chmod 600 .ssh chmod 700 .ssh/.ssh/authorized_keys

如果不成功,就是 /etc/ssh/sshd_config 的某些配置不正确

在 namenode/datanode 分别安装 java 环境

yum install java java-devel

安装 hadoop-2.9.0

hadoop 的安装为绿色安装。规划安装目录为 /hadoop-2.9.0

  • 下载 wget https://dist.apache.org/repos/dist/release/hadoop/common/hadoop-2.9.0/hadoop-2.9.0.tar.gz

修改配置文件 /hadoop-2.9.0/etc/hadoop/

  • slaves

添加 datanode, 每行一行,本例中: datanode

  • core-site.xml
1
2
3
4
5
6
7
8
9
10
<configuration>
<property>
<name>fs.defaultFS</name>
<value>hdfs://namenode:9000</value>
</property>
<property>
<name>hadoop.tmp.dir</name>
<value>/hadoop-2.9.0/tmp</value>
</property>
</configuration>
  • hdfs-site.xml
1
2
3
4
5
6
7
8
9
10
<configuration>
<property>
<name>dfs.namenode.name.dir</name>
<value>/hadoop-2.9.0/dfs/name</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>/hadoop-2.9.0/dfs/data</value>
</property>
</configuration>
  • mapred-site.xml 首先要 cp etc/hadoop/mapred-site.xml.template etc/hadoop/mapred-site.xml
1
2
3
4
5
6
<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
</configuration>
  • yarn-site.xml
1
2
3
4
5
6
7
8
9
10
<configuration>
<property>
<name>yarn.resourcemanager.hostname</name>
<value>namenode</value>
</property>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
</configuration>
  • hadoop-env.sh
1
export JAVA_HOME=/usr/lib/jvm/java-openjdk

把 namenode 的 hadoop 软件发送到各个datanode

1
2
3
tar -zcf ./hadoop-2.9.0.tar.gz ./hadoop-2.9.0
scp hadoop-2.9.0.tar.gz datanode:/
tar -xvf hadoop-2.9.0.tar.gz

设置 JAVA_HOME

1
2
3
4
#set java environment
export JAVA_HOME=/usr/lib/jvm/java-openjdk
export HADOOP_HOME=/home/wwwroot/hadoop/hadoop-2.9.0
export PATH=${JAVA_HOME}/bin:${HADOOP_HOME}/sbin:${HADOOP_HOME}/bin:$PATH

格式化文件系统

hdfs namenode -format

启动

1
2
3
start-dfs.sh
start-yarn.sh
mr-jobhistory-daemon.sh start historyserver

成功后通过 jps 命令可以在 namenode 看到

1
2
3
4
NameNode、
ResourceManager、
SecondrryNameNode、
JobHistoryServer

在 datanode 通过 jps 可以看到

1
2
DataNode
NodeManager

参考:

QA

开启分布式后,运行 job 会感觉慢?

因为自己的机器内存不足,所以开发测试情况下,建议用单机模式。

如何让 system.out.println() 打印到控制台?

默认是可以打印出的,如果没有打印出,应该是之前配置过 mapred-site.xml。 把里面的配置删掉或者注释掉,就可以打印到控制台了。

centos 配置 JAVA_HOME

安装

1
yum install java java-devel

默认安装在 /usr/lib/jvm/java-openjdk

通过 ll 命令可看到 java-openjdk 是个软连接

1
2
[root@namenode jvm]# ll /usr/lib/jvm/java-openjdk
lrwxrwxrwx 1 root root 34 Feb 20 14:28 /usr/lib/jvm/java-openjdk -> /etc/alternatives/java_sdk_openjdk

再通过 ll /etc/alternatives/java_sdk_openjdk 就可以到实际的目录地址

1
2
[root@namenode jvm]# ll /etc/alternatives/java_sdk_openjdk
lrwxrwxrwx 1 root root 60 Feb 20 14:28 /etc/alternatives/java_sdk_openjdk -> /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-0.b14.el7_4.x86_64

配置 JAVA_HOME

1
2
3
4
vim ~/.bashrc

#set java environment
export JAVA_HOME=/usr/lib/jvm/java-openjdk

问题:

  • 当遇到 JAVA_HOME 版本不对?

这就需要更改 /etc/alternatives/java_sdk_openjdk 实际的软连接地址。

或者直接在 ~/.bashrc 里直接修改为

1
2
#set java environment
export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-0.b14.el7_4.x86_64

但是要注意查看 java-1.8.0-openjdk-1.8.0.161-0.b14.el7_4.x86_64 目录下是不是 jre 目录,如果有需要如下配置才能生效:

1
2
#set java environment
export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-0.b14.el7_4.x86_64/jre

学习第一阶段目标

目标

Flume + Hadoop(Java) + Hive + Sqoop 的离线分析系统

学习顺序

了解整体架构

  • 离线分析系统的整体架构
  • 每个部分数据传输流程
参考文章

Java 基本语法

  • 基本数据类型
  • 面向对象 资料
验收方式
  • 能看懂 Hadoop 文档自带的 WordCount 应用的代码。

Hadoop

  • HDFS 分布式文件系统
  • 配置单点,集群 熟练理解文档自带的 WordCount 应用。1.x中文2.x
  • 理解示例 WordCount 使用到的 Hadoop 接口。学会查看 Hadoop类库
验收方式
  • 把淘车猫的 access_log 上传到 Hadoop 的文件系统中。
  • 能使用 Hadoop 的文件系统常用命令。文件的创建,移动等等

Java 接口函数

  • 集合框架 资料
  • 处理各种字符串的对象函数 找一些小项目源码多练习
  • ArrayList, HashSet, Map 等数据结构
验收方式
  • 能看到 Hadoop 文档自带应用 WordCount 2.0 版本
  • 能使用集合框架写出学习资料里最后的 扑克 应用。

Hive

  • HiveSql 学习
验收方式
  • 可以通过文本数据创建 HiveSql 表,并可以做排序等操作
  • 能实现 这里 的例子

Sqoop

  • 具体学习的内容还没看
验收方式
  • 能把 Hive 数据导入进 Mysql。

Flume

  • Flume 配置
验收方式
  • 可以把 access.log 文件,导入进 Hadoop 文件系统。

综合 Demo,统计 Uv,Pv (1周)

  • 统计分析淘车猫的 access_log

学习第二阶段目标

to be continue …

16年的时候整理过一篇IOC的实现原理,在进一步之前可先 查看
然后结合laravel再来看IOC的实现,便会有一种 原来是这样 的感受。从而也会对一直不接触的设计模式有根清晰的认识。

先来看几个名词

依赖注入

这是一个很花哨的名词,如很多设计模式一样,但不了解具体实现的时候就会觉得雨里雾里,不知所云。

先来看一段简单的代码,也是很「经典」的代码

class UserController
{
    publc function getUserInfo($id)
    {
           $model = new UserDao;

           return $model->where('id', $id)->first();
    }
}

也许你看出来了,所谓「经典」并不是因为代码有奇妙之处,而是,这样的代码在很多「工程师」的代码里,随处可见。

这样的代码一个很明显的问题便是,「高级代码」依赖「低级代码」,「低级代码」如果出错,哪「高级代码」就完全用不了。

{tip} 「高级代码」 业务逻辑代码实现,也就是最终呈现给用户的代码。 「低级代码」 数据库操作,文件操作,网络操作等一切为业务服务的代码。

来看一段改进

class UserController
{
    protected $user;

    publc function getUserInfo(User $user, $id)
    {         
           return $user->where('id', $id)->first();
    }
}

修改以后的业务代码,不需要知道数据从何而来,只需要知道从一个 User 对象能获取到所需要的数据。

这样的方式便叫「依赖注入」,「我」依赖你,但并不是离了「你」就不能活。

控制反转

控制反转是依赖注入的一种改进,在上面的代码中,「高级代码」 想要获取到「低级代码」数据,必须手动传入「User」,这样当「User」作出修改,哪「高级代码」依赖的部分也要做出修改。

利用「控制反转」来实现

class UserController
{
    protected $user;

    publc function getUserInfo(UserInterface $user, $id)
    {         
           return $user->where('id', $id)->first();
    }
}

这里注入的是一个「接口」,「高级代码」不依赖具体的实现,而依赖的是实现的接口。这样无论「低级代码」作出怎样的修改,「高级代码」都不需要做任何修改。

这就是「面向对象」常说的:要面向接口编程,而不是面向实现。

Laravel容器实现

容器的终极目标就是解决「依赖」。让「低级代码」的实现,不会出现在「高级代码」的逻辑中。

通过Laravel的「Route」模块,来分析容器的实现。看 routes/web.php 的内容

以前,每当学习一个框架的时候,首先看的是控制器,因为比较容易看到效果。

Route::get('/',funtion(){
     return view('welcome');
})

初学者往往会以为这里就是框架的入口—因为我们写的业务代码确实是从这里开始的,从而导致了很多人认为请求直接就到了这里。其实不然。

既然 web.php 里可以使用 Route::method ,通过 这篇文章 可以知道,既然这里可以使用,必然有地方进行了「绑定」。

有一个现成的「轮子」,Illuminate\Container (容器) Laravel框架的核心程序便是继承了这个「轮子」,然后把「其他模块」通过「绑定」的方式,组合进了框架。所以 Laravel 的核心代码很少。其中「路由」,「请求」,「响应」等等各个模块都是通过「绑定」的方式组合到一起实现强大的功能。

在进一步之前,确保你已经看懂了Laravel的文档,请求的生命周期,很多时候最好的学习资料是框架本身提供的文档。

Laravel 通过 Illuminate\Container 提供的 bind instance singleton 等等方式进行「注入」到容器里,再通过make 获取到注入的「实例」「匿名函数」「接口实现」等。

本来打算写每个函数的用法,然后发现,无论怎么写,都没有官方文档总结的到位。遂放弃,所以建议多读几遍官方文档吧。

接着上面的 Route 继续看,通过查看 Illuminate\FoundationApplication 可以看到,Laravel通过「Container」注入了路由的「服务提供者」,并把「Container」传给了「服务提供者」

$this->register(new RoutingServiceProvider($this));

而后由 RoutingServiceProvider 自己注册实现和所需要的依赖。

class RoutingServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->registerRouter();

        $this->registerUrlGenerator();

        $this->registerRedirector();

        $this->registerPsrRequest();

        $this->registerPsrResponse();

        $this->registerResponseFactory();
    }

这时候,我们便可以通过 $app['route'] 或者 $app->route 来获取到路由的实现,这会执行「容器」的 make 方法。获取到之前注册的内容。

而在web.php里没有这样写,这就涉及到另一个概念 Facade

看到配置文件 bootstrap/app.php

aliases' => [
    'Route' => Illuminate\Support\Facades\Route::class
]

当使用 Route 的实际调用的是 Illuminate\Support\Facades\Route::class , Route::method

执行的就是 Illuminate\Support\Facades::__callStatic

public static function __callStatic($method, $args)
{
    $instance = static::getFacadeRoot();

    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }

    return $instance->$method(...$args);
}

其中

$instance = static::getFacadeRoot();

最终调用

protected static function resolveFacadeInstance($name)
{
    if (is_object($name)) {
        return $name;
    }

    if (isset(static::$resolvedInstance[$name])) {
        return static::$resolvedInstance[$name];
    }

    return static::$resolvedInstance[$name] = static::$app[$name];
}

可以看到,其实就是调用了 $app[name] ;

到此,通过容器的分析,便可以了解laravel的核心概念了。

一年过去了,竟然没有什么一件事情可以称为记忆深刻。感觉记忆好模糊的一年啊。

从6月份离开联想到现在,这半年没做过能记得名字的项目。

没有实质性的改变,想写个年终总结都不知道从何写起。一年过了,跟没过似的。只蹉跎了岁月,却毫无长进。

但是。该写个规划啊。毕竟2017第一天上班日。