作者归档:admin

Laravel 管道详解

函数http://php.net/array_reduce用法(http://php.net/array_reduce
):

// array_reduce() 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。
mixed array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] )

第二参数的$callback调用格式:

//$carry携带上次迭代里的值; 如果本次迭代是第一次,那么这个值是 initial
//$item携带了本次迭代的值
mixed callback ( mixed $carry , mixed $item )

1 如果$array为空,直接返回$initial
2 如果$array不为空,那么$initial作为第一次迭代时的$carry
3 每次迭代调用的$callback的结果作为下次迭代调用$callback的$carry

这里主要是第三点,经过特殊处理,可以产生多层嵌套(C包装了B,B包装了A)。

列子:

$arr = [3, 2, 1];
$arr = [];
$fn = function ($carry, $hold) {
    return function() use ($carry, $hold) {
        echo "$hold -- Start\n";
        $carry();
        echo "$hold -- End \n";
    };
};
$init = function () {
    echo "init\n";
};
$callback = array_reduce($arr, $fn, $init);
$callback();

输出:

// $arr等于空时
init

// $arr = [3,2,1]时
1 -- Start
2 -- Start
3 -- Start
init
3 -- End 
2 -- End 
1 -- End

各种情况分析:
1 当$arr为空时,直接返回$init,这里返回一个闭包函数,执行后直接输出”init”字符串
2 当$arr等于[3]时,第一个次迭代,相当于如下代码:

// 相当于
$fn($init, 3);
// 相当于
$fn(function () {
    echo "init\n";
}, 3);
// 相当于
function (function () {
    echo "init\n";
}, 3);
// 相当于
function() use(function () {
    echo "init\n";
}, 3) {
    echo "$hold -- Start\n";
    (function () {
        echo "init\n";
    })();
    echo "$hold -- End \n";
}
// 最后运行这个闭包,输出
3 -- Start
init
3 -- End

3 对于$arr大于1的情况也是类似,最终会得到类似这样的结构

(function() use($carray, 1) {
    echo "1 -- Start\n";
    (function () use ($array, 2) {
        echo "2 -- Start\n";
        (function () use($carray, 1) {
            echo "3 -- Start\n";
            (function () {
                echo "init\n";
            })();
            echo "3 -- End\n";
        })();
        echo "2 -- End\n";
    })();
    echo "1 -- End\n";
})();

使用闭包,把每次迭代过程中的变量锁定,执行时,从外层逐层剥离。这个就是管道的实现方式。

在Laravel中,包Illuminate\Pipeline的管道实现原理与以上的描述类似。而管道的应用,最经典的就是中间件的设计。请求在分发到具体的路由前(或之后),可以设置其经过一些列的中间,要经过的中间件,就是一个个管道,在任何时刻,都可以非常无痛的插入和删除某个中间件,以下是模拟过程:

class VerifyCsrfToken
{
    public static function handle(Closure $next)
    {
        echo "VerifyCsrfToken\n";
        $next();
    }
}

class ShareErrorsFromSession
{
    public static function handle(Closure $next)
    {
        echo "ShareErrorsFromSession\n";
        $next();
    }
}

class StartSession
{
    public static function handle(Closure $next)
    {
        echo "StartSession -- Start\n";
        $next();
        echo "StartSession -- End\n";
    }
}

class AddQueuedCookiesToResponse
{
    public static function handle(Closure $next)
    {
        $next();
        echo "AddQueuedCookiesToResponse\n";
    }
}

class EncryptCookies
{
    public static function handle(Closure $next)
    {
        echo "EncryptCookies -- Start\n";
        $next();
        echo "EncryptCookies -- End\n";
    }
}

class CheckForMaintenanceMode
{
    public static function handle(Closure $next)
    {
        echo "CheckForMaintenanceMode\n";
        $next();
    }
}

class pipeline
{
    protected $hold = null;

    protected $pipes = [];

    public function send($hold)
    {
        $this->hold = $hold;
        return $this;
    }

    public function though(array $pipes)
    {
        $this->pipes = $pipes;
        return $this;
    }

    public function then(Closure $to)
    {
        $fn = function ($stack, $pipe) {
            return function() use ($stack, $pipe) {
                return $pipe::handle($stack);
            };
        };
        $callBack = array_reduce($this->pipes, $fn, $to);

        return $callBack();
    }
}

//////////////////////////////////////
$hold = [];
$pipes = array_reverse([
//    "CheckForMaintenanceMode",
//    "EncryptCookies",
//    "AddQueuedCookiesToResponse",
//    "StartSession",
//    "ShareErrorsFromSession",
//    "VerifyCsrfToken"
]);
$result = (new Pipeline())->though($pipes)->then(function () {
    echo "--------Request to Route------------\n";
});

这样,我们自定义的中间件(数组顺序),如果希望在路由前运行,就在$next()前执行,如果希望路由后执行,就在$next()后运行。当然,在执行某个中间件时(管道),也可以直接return,这样后面的代码就不会执行。

以上的演示代码已经把管道的实现基本说清楚,以下是Illuminate\Pipeline的用法:

<?php

require __DIR__.'/../vendor/autoload.php';

class MyPipe
{
    public function handle($request, $next, $param1 = 1, $param2 = 2)
    {
        echo "MyPipe Handle -- request is $request, param1 is $param1, param2 is $param2 \n";
        $request++;
        $next($request);
    }

    public function fire($request, $next, $param1 = 1, $param2 = 2)
    {
        echo "MyPipe Fire -- request is $request, param1 is $param1, param2 is $param2 \n";
        $request++;
        $next($request);
    }
}
$container = new Illuminate\Container\Container();

$request = 10;
// 三种使用方式
$pipes = [
    // 直接传递一个闭包
    function($request, $next) {
        $request++;
        $next($request);
    },
    // 直接生成一个包含"handle方法的对象",无法传递额外参数
    new MyPipe(),
    // 管道对象生成时需要把容器对象传入,因为需要依靠容器根据字符串类make对象
    // 直接指定一个包含"handle"方法的类名称
    // handle方法如果有超过2个参数,超过的参数可以在冒号后给出,并以逗号分隔多个参数
    "MyPipe:11,22"
];

// 更换默认调用的方法
$pipeline = new Illuminate\Pipeline\Pipeline($container);
$pipeline->via("fire")->send($request)->through($pipes)->then(function ($request) {
    echo "request is $request\n";
    echo "Finish ---\n";
});

// 使用默认的handle方法
$pipeline = new Illuminate\Pipeline\Pipeline($container);
$pipeline->send($request)->through($pipes)->then(function ($request) {
    echo "request is $request\n";
    echo "Finish ---\n";
});

// 输出
MyPipe Fire -- request is 11, param1 is 1, param2 is 2 
MyPipe Fire -- request is 12, param1 is 11, param2 is 22 
request is 13
Finish ---

MyPipe Handle -- request is 11, param1 is 1, param2 is 2 
MyPipe Handle -- request is 12, param1 is 11, param2 is 22 
request is 13
Finish ---

闭包函数应用:

$destination = function () {};

$fn = function ($passable) use ($destination) {
    return $destination($passable);
};
$passable = 1;
// $destination() 和$destination($passable)等同
// $destination($passable) 和 $fn($passable)等同

当要把一个闭包以一个统一的格式执行时(总是带一个或多个参数),通常需要做这样的转换。$destination()这个闭包函数定义时不需要传递参数,但是在调用时传递传输参数过去也不会报错。

demo($destination) 
{
    return function ($passable) use ($destination) {
        return $destination($passable);
    }
}
//
demo($destination) === function ($passable) use ($destination) {
    return $destination($passable);
};
// 调用时就必须是fn(param)格式,外部参数注入到了闭包,闭包中就可以使用到外部传递进来的参数。

传递一个闭包函数给一个函数,这个函数返回了一个包装了这个闭包函数的闭包函数,这样外部的闭包函数执行,就可以把外部的变量传递给内部的闭包函数。

其它管道实现参考:
https://github.com/thephpleague/pipeline

Laravel 认证详解


管理器
管理多个拦截器,通过给定拦截器的名称,可以返回或生产拦截器并返回,那么管理器必定是一个工厂(生产拦截器)。管理器管理多个拦截器,那么必定就有一个默认拦截器的概念。

拦截器
拦截器要认证用户(用户登录),首先客户端需要提供凭证(比如客户端提供的token或对应的用户session中的用户id),如果没有凭证,就直接跳转到登录页(或返回认证失败信息),然后是拿到凭证后(如果是非一次性认证,这个时候还会到持久介质中检查是否已经登录,登录就直接通过 – 持久介质一般就是指session了),还需要知道到什么地方进行比对,这时需要有一个数据源(比如用户表)。这个数据源就是提供者,它向拦截器按照一致的方法提供用户实例。拦截器对于已经通过认证的用户,可能还需要保存认证凭证(下次请求直接通过),有保存认证凭证,当然还要包括注销凭证(用户登出)。

提供者
拦截器面对提供者,通过向提供者获取用户实例,那么提供者必须按照一致的方式编写(这样不同的拦截器就可以调用不同的提供者)。提供者是数据源的抽象,对外提供统一接口,比如根据某个一致的方法返回用户实例。

用户实例
提供者提供用户实例,拦截器使用这个用户实例来比对,很明显,如果用户实例没有一定的规范,也是不行的,比如要比对什么字段不能定死,但是可以定死方法,这个就是接口规范。

总结:
管理器是一个工厂,可以管理和生成拦截器。拦截器需要一个提供者,依赖提供者提供的用户来认证用户。提供者被拦截器调用,它需要提供一致的接口,以使得它可以插入到不同的拦截器中。提供者提供的用户实例也是面向拦截器的,为了能适应不同的拦截器,用户实例也需要按照一致的接口来实现。

由于有一致的接口,所以一个拦截器,可以更换提供者(比如使用不同的数据)。一个提供者,可以插入到不同的拦截器(比如拦截器都使用同一个提供者)。

可扩展:
对于一些常用的拦截器,比如token和session,可以预先实现,同理,对于提供者,也可以预先实现。如果需要插入自定义的拦截器或提供者,只需要按照预定实现相关方法就可以了。

以上就是用户认证的理论知识。以下就是针对这些理论知识进行展开。

管理器 – Illuminate\Auth\AuthManager:

这个类提供了怎么创建默认的Guard的方法(createSessionDriver、createTokenDriver),和怎么创建Provider的方法(createDatabaseProvider),还提供了如何插入自定义的Guard的方法(extend()方法)和插入自定义Provider的方法(provider()方法)。Guard实例的生成,对应的Provider实例必须先生成。

AuthManager中管理不同的Guard,Guard实例的生成需要知道Driver(就是怎么生成Guard实例,比如session,api),还需要知道Provider(数据源),Provider内置支持Database和Eloquent这两种类型,根据类型产生数据源对象(需要实现对应接口)。另外,还有一个默认Guard的概念。有了Guard实例后,需要从Guard实例中取回用户实例,AuthManager中记录默认Guard的用户实例(userResolver)

用户登录认证,首先需要拦截所有的请求,判断是否登录,如果没有登录,跳转到登录页,登录成功后,记录登录信息到会话(下次就知道已登录);如果登录了,跳转到首页, 还可以取到用户实例。

这里的拦截器就是Guard, 登录成功后保存信息,取回用户实例,是Guard需要实现的,另外,如何认证用户需要依赖传递给Guard的Provider,Guard收集认证信息,到Provider中查找比对做出判断。

提供者 – 用户提供者
考虑这样的应用场景:有多个系统公用一套管理员信息,即管理员A用同一套信息可以登录系统1,也可以登录系统2。

这里的管理员需要单独出来,并对外提供API,系统1和系统2以统一的方式调用这些API进行用户认证。

首先需要定义一个UserProriver,需要实现Illuminate\Contracts\Auth\UserProvider接口:

// 通过唯一标示符获取认证模型
public function retrieveById($identifier);
// 通过唯一标示符remember token获取模型
public function retrieveByToken($identifier, $token);
// 通过给定的认证模型更新remember token
public function updateRememberToken(Authenticatable $user, $token);
// 通过给定的凭证获取用户,比如 email 或用户名等等
public function retrieveByCredentials(array $credentials);
// 认证给定的用户和给定的凭证是否符合
public function validateCredentials(Authenticatable $user, array $credentials);

自定义的UserProriver需要实现这些方法(在其中调用外部API),从这个Provider返回的用户实例必须实现了Illuminate\Contracts\Auth\Authenticatable接口的实例(可以认证的):

<?php

namespace Illuminate\Contracts\Auth;

interface Authenticatable
{
    public function getAuthIdentifierName();
    public function getAuthIdentifier();
    public function getAuthPassword();
    public function getRememberToken();
    public function setRememberToken($value);
    public function getRememberTokenName();
}

注:App\User实现了该接口(可认证),实际是使用了Illuminate\Auth\Authenticatable特性。

Illuminate\Auth\GenericUser也实现了这个接口,可以直接套用(DatabaseUserProvider就用它来包装用户实例),否则需要自己实现(比如字段名称不一样),具体实现可以参考Illuminate\Auth\DatabaseUserProvider。

图中MyUser是一个自定义实现,除非需要自定义各种字段,否则没有必要,直接使用GenericUser来对应用户实例即可。

接下来是调用AuthManager的provider()方法来定义新的Provider(数据源):

// App\Providers\AuthServiceProvider
public function boot()
    {
        $this->registerPolicies();

        \Auth::provider('my-user', function() {
            return new MyUserProvider();    // 返回自定义的 user provider
        });
    }

然后修改配置,让Guard使用my-user作为Provider:

// app/config/auth.php
'guards' => [
        'web2' => [
            'driver' => 'session',
            'provider' => 'my-user'  //名字可以随意,只要在providers中找到
        ],
],
'providers' => [
        'my-user' => [
            'driver' => 'my-user'  //名字必须和在\Auth::provider()定义时的一样
        ]
],

这里的’providers’中的’my-user’是否需要其它的参数(driver参数必须,并且和定义时的值一样),是由自定义的Provider决定的,可以根据情况添加。

设置 config\auth.php 的 defaults.guard 为web2,就可以使用自定义的Provider进行认证了。

拦截器 – Guard
考虑这样的应用场景:如果需要修改登录认证逻辑,比如对认证做一些额外的处理,默认提供的Guard不能满足的情况。

Illuminate\Contracts\Auth\StatefulGuard接口在Illuminate\Contracts\Auth\Guard
上提供了额外方法,比如:login() attempt() logout()等(Session认证必须)。

每个Guard实际都注入了Illuminate\Auth\GuardHelpers, 这是一些辅助方法,比如:check() 、guest()通用的方式实现 。Guard中进行拦截是实际是跟对应的UserProvider进行交互,取回用户实例(必须实现Illuminate\Contracts\Auth\Authenticatable接口),

Illuminate\Contracts\Auth\Guard接口:

// 判断当前用户是否登录
public function check();
// 判断当前用户是否是游客(未登录)
public function guest();
// 获取当前认证的用户
public function user();
// 获取当前认证用户的 id,严格来说不一定是 id,应该是上个模型中定义的唯一的字段名
public function id();
// 根据提供的消息认证用户
public function validate(array $credentials = []);
// 设置当前用户
public function setUser(Authenticatable $user);

Illuminate\Contracts\Auth\StatefulGuard继承Guard接口,还提供了:

// 尝试根据提供的凭证验证用户是否合法
public function attempt(array $credentials = [], $remember = false);
// 一次性登录,不记录session or cookie
public function once(array $credentials = []);
// 登录用户,通常在验证成功后记录 session 和 cookie 
public function login(Authenticatable $user, $remember = false);
// 使用用户 id 登录
public function loginUsingId($id, $remember = false);
// 使用用户 ID 登录,但是不记录 session 和 cookie
public function onceUsingId($id);
// 通过 cookie 中的 remember token 自动登录
public function viaRemember();
// 登出
public function logout();

预定义的拦截器:
1 RequestGuard
Illuminate\Auth\RequestGuard
RequestGuard是一个非常简单的Guard。RequestGuard 是通过传入一个闭包来认证的。可以通过调用 Auth::viaRequest 添加一个自定义的 RequestGuard.(每个请求,都执行这个闭包,这个闭包负责怎么认证用户,当然,数据来源,怎么比对数据,都需要在这个闭包中完成)

2 SessionGuard
Illuminate\Auth\SessionGuard
SessionGuard 是 Laravel web 认证默认的 guard(用户认证后,记录到session,第二次请求就不会再次认证).

3 TokenGuard
Illuminate\Auth\TokenGuard
TokenGuard 适用于无状态 api 认证,通过 token 认证(请求中携带token,用token认证用户,每次请求都会进行认证).

要实现自定义的Guard,可以参考以上的实现,然后调用AuthManager的extend()方法来定义新的Guard(Driver):

// App\Providers\AuthServiceProvider
public function boot()
    {
        $this->registerPolicies();

        \Auth::extend('web2', function(){
              return new MyGuard();
        });
    }

Guard需要使用到Provider(构造函数中注入),完成验证,检查等操作。

另外
Laravel支持在认证过程中触发多种事件,可以在的EventServiceProvider中监听这些事件:

protected $listen = [
    'Illuminate\Auth\Events\Registered' => [
        'App\Listeners\LogRegisteredUser',
    ],


    'Illuminate\Auth\Events\Attempting' => [
        'App\Listeners\LogAuthenticationAttempt',
    ],

    'Illuminate\Auth\Events\Authenticated' => [
        'App\Listeners\LogAuthenticated',
    ],

    'Illuminate\Auth\Events\Login' => [
        'App\Listeners\LogSuccessfulLogin',
    ],

    'Illuminate\Auth\Events\Failed' => [
    'App\Listeners\LogFailedLogin',
],

    'Illuminate\Auth\Events\Logout' => [
        'App\Listeners\LogSuccessfulLogout',
    ],

    'Illuminate\Auth\Events\Lockout' => [
        'App\Listeners\LogLockout',
    ],

    'Illuminate\Auth\Events\PasswordReset' => [
        'App\Listeners\LogPasswordReset',
    ],
];

利用SSH的用户配置文件管理SSH会话

一般,在Shell中可以如下远程登录一台机器:

ssh user@hostname -p port

然后输入密码(如果有放置公钥则直接过)。

但是如果有很多机器要操作,就可以利用SSH中的用户配置文件来管理会话(man ssh_config)。

SSH的用户配置文件是放在当前用户根目录下的.ssh文件夹里(~/.ssh/config,不存在可新建),其配置写法如下:

Host			别名
	HostName	主机名
	Port		端口
	User		用户名
	IdentityFile	秘钥文件的路径(如果私钥不是放置默认位置,这里可以指定)

配置好后就可以使用别名登录:

ssh 别名
scp 别名:~/test .

如果有多个机器,对应在配置中写入。

可见,SSH的过程,总是首先读取config文件,然后解析,然后看看是否匹配别名,如果匹配,就使用匹配的别名对应的配置登录。

生成SSH秘钥:

root@vfeelit:~# ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): 
Created directory '/root/.ssh'.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.

把公钥放到远程机器的~/.ssh/authorized_keys文件中。

Bash Shell 变量

在Bash当中,当一个变量名称未被设置时,默认的内容是空的。

变量的设置规则:
1) 变量与变量内容以一个等号连接,等号两边不能接空格,注意,如果有空格则是逻辑比较
2) 变量名称只能是英文字母和数字,但开头不能是数字
3) 变量内容如含有空格,需要使用单引号或双引号括起来(如没有引号括起来,但是包含了转义,最终会转义输出),但双引号内特殊字符保持本性,单引号内的任何字符都是一般文本
4) 可以使用转义符将特殊字符变成一般字符
5) 如需要引用其它命令的结果,可以使用反单引号或$()语法,比如`uname –r`和$(uname -r)等同
6) 如要为变量增加内容,可以使用$变量名或${变量名}累加内容(不需要所谓的连接符)
7) 如变量需要在其它子进程中执行,需要以export来使变量变成环境变量:export PATH。
8) 通常大写字符为系统默认变量
9) 取消变量的方法是使用unset 变量名称

环境变量与自定义变量
在一个Shell中启用一个子Shell,子Shell中会继承父Shell的环境变量,但是自定义变量不会被继承(除非自定义的变量声明为环境变量)。
查看当前Shell中的环境变量,可以使用env命令。
查看当前Shell中的所有变量(包括环境变量),可以使用set命令。
命令set可以查看已经存在的Shell变量,以及设置Shell变量的新变量值(可改环境变量值),但是不能定义新的Shell变量。
命令declare用来定义自定义变量,如果需要把自定义变量变成环境变量,需要使用export变量名;如果需要直接声明一个环境变量,可以使用declare +x格式。
如果需要直接设置一个环境变量,可以使用export var=value格式。

自定义变量是指用户自己定义的变量,定义时可以声明它为环境变量(+x),也可以之后重新以declare +x格式变换成环境变量,或以export方式将其变为环境变量。
自定义变量不会因为它变成了环境变量而脱离它是自定义变量的事实。
变量变成环境变量后,可以set一个新值,可以unset,但是无法去掉环境变量标签(即:总是环境变量,也总是自定义变量)。

#查看环境变量
env

#查看环境变量和自定义变量
set

#查看自定义变量(不管定义是不是环境变量)
#export var=value,也是自定义变量
declare

#修改变量值(var必须先存着)
set var=value

#定义一个环境变量
export var=value

#定义一个自定义变量
declare var=value

#把自定义变量转换为环境变量
export var

#定义一个环境变量(或把变量变成环境变量)
declare -x var

#取消变量
unset var

当export一个变量,实际应该调用的是declare +x,所以export默认的输出是自定义变量变成环境变量的列表:

export
declare -x USER="root"
declare -x XDG_RUNTIME_DIR="/run/user/0"
declare -x XDG_SESSION_ID="2237"
declare -x var="value"

总之,用户自定义的变量,永远是自定义的,这个自定义的变量可以变换为环境变量(但是还是自定义的)。env查看环境变量,declare查看自定义变量(包括自定义变成环境变量的变量),set查看所有变量(环境变量和自定义变量的并集),export可以查看有哪些自定义变量升级为环境变量。 可以使用declare定义变量(+x声明为环境变量),export可以把变量转换为环境变量(实际是declare +x的使用),set对存在的变量设置新的值。一般使用declare定义一个自定义变量(或定义时指定值),export定义一个环境变量(或定义时指定值,export是declare的一种用法),用set来改变变量值。

如果一个变量同时出现在env和declare的输出结果中,说明这个变量是从自定义变量升级为环境变量的。如果只出现在env结果中,说明不是由自定义变量升级而来。

如下图:

特别用法:

#若指令返回值不等于0,则立即退出Shell
set -e

Laravel Envoy 详解

Envoy: 使节,外交官;全权公使;谈判代表

简单描述这个包的作用:以一种更加友好的方式,在本地操作远程服务器。

Laravel Envoy是一个标准的PHP组件,被定义为一个命令行工具,可以使用composer全局安装,也可以安装到具体项目中。

地址:
https://packagist.org/packages/laravel/envoy

检查composer.json文件:

"autoload": {
    "classmap": ["src"]
},
"require": {
    "illuminate/support": "~4.1 || ~5.0",
    "nategood/httpful": "~0.2",
    "symfony/console": "~3.0 || ~4.0",
    "symfony/process": "~3.0 || ~4.0"
}, 
"bin": [
    "envoy"
],

首先,这个包的类是以classmap方式直接映射的,另外它依赖4个包,其中symfony/console用来建立命令行工具的,symfony/process用来操作进程。

另外,bin指定了envoy,表明这个包下有一个叫envoy的文件,告诉Composer在vendor/bin中建立对应的符号链接(认为是执行文件)。

安装:

// 全局安装
composer global require laravel/envoy

// 在具体项目中安装
composer require laravel/envoy

不管以哪种方式安装,最终都需要在命令行中可以找到envoy这个工具,所以,可以把~/.composer/vendor/bin加入到PATH中:

vi .bash_profile
export PATH="/Users/vft/.composer/vendor/bin:/usr/local/sbin:$PATH"

用法(参考官方文档):

// 安装包中的Envoy.blade.php就是定义任务的地方(默认存在)
// 编写任务
@servers(['web' => 'user@192.168.1.1'])

@task('foo', ['on' => 'web'])
    ls -la
@endtask

任务foo是在web这台机器上运行(意思是登录到这个机器运行指定的任务)。

在运行任务前先执行一段PHP代码:

@setup
    $now = new DateTime();
    $environment = isset($env) ? $env : "testing";
@endsetup

引入其它文件:

@include('vendor/autoload.php')

@task('foo')
    # ...
@endtask

传递变量到命令:

envoy run deploy --branch=master
@servers(['web' => '192.168.1.1'])

@task('deploy', ['on' => 'web'])
    cd site
    @if ($branch)
        git pull origin {{ $branch }}
    @endif
    php artisan migrate
@endtask

任务分组:

@servers(['web' => '192.168.1.1'])

@story('deploy')
    git
    composer
@endstory

@task('git')
    git pull origin master
@endtask

@task('composer')
    composer install
@endtask

// 运行
envoy run deploy

任务分组用story指令来完成,所谓story,其实也可以看做是一个任务(执行时就看做是一个任务),不过它会依次运行分组里面的任务而已。

使用多个服务器:

@servers(['web-1' => '192.168.1.1', 'web-2' => '192.168.1.2'])

@task('deploy', ['on' => ['web-1', 'web-2']])
    cd site
    git pull origin {{ $branch }}
    php artisan migrate
@endtask

// 并行运行
@servers(['web-1' => '192.168.1.1', 'web-2' => '192.168.1.2'])

@task('deploy', ['on' => ['web-1', 'web-2'], 'parallel' => true])
    cd site
    git pull origin {{ $branch }}
    php artisan migrate
@endtask

在使用多个服务器的情况下,默认是一个服务器上的任务运行完成后,开启另一台服务器任务的运行,如果需要同时运行,那么就需要在定义任务时添加parallel为true。

运行任务:

./envoy run task

任务运行前确认:

@task('deploy', ['on' => 'web', 'confirm' => true])
    cd site
    git pull origin {{ $branch }}
    php artisan migrate
@endtask

命令执行后发送通知:

@after
    @slack('webhook-url', '#bots')
@endafter

发送通知这个不过是调用外部程序预留的钩子,达到通知外部的目的。

以下跟踪以下具体实现:
对应的文件:vendor/laravel/envoy/envoy是入口文件,vendor/laravel/envoy/ Envoy.blade.php就是定义任务的地方。

首先查看vendor/laravel/envoy/envoy:

#!/usr/bin/env php
<?php

if (file_exists(__DIR__.'/vendor/autoload.php')) {
    require __DIR__.'/vendor/autoload.php';
} else {
    require __DIR__.'/../../autoload.php';
}

$app = new Symfony\Component\Console\Application('Laravel Envoy', '1.4.1');

$app->add(new Laravel\Envoy\Console\RunCommand);
$app->add(new Laravel\Envoy\Console\SshCommand);
$app->add(new Laravel\Envoy\Console\InitCommand);
$app->add(new Laravel\Envoy\Console\TasksCommand);

$app->run();

第一行文件:#!/usr/bin/env php,找到PHP,如果环境变量中找不到PHP,就需要指定运行该脚本。

接下来是把autoload.php包含进来,命令可能是vendor/bin中的envoy命令,也可能是组件中的envoy命令,所以包含autoload.php需要适配这两种情况。

然后就是一个典型symfony/console命令行工具:先建立一个Application,然后往Application中添加命令,然后运行命令。

关于symfony/console包的使用,参考:
http://symfonychina.com/doc/current/console.html

运行envoy:

./envoy 
Laravel Envoy 1.4.1

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  help   Displays help for a command
  init   Create a new Envoy file in the current directory.
  list   Lists commands
  run    Run an Envoy task.
  ssh    Connect to an Envoy server.
  tasks  Lists all Envoy tasks and macros.

命令envoy的用法是:envoy [options] [arguments], 这里的optins列表是默认添加的,在可用命令部分有help和list都是默认提供的(直接输入envoy时,默认就是调用list命令),主动添加的只有:init, tasks, ssh, run。

以下看看具体的命令实现,首先,添加一个命令,命令周边的操作都一样,而命令只需要实现configure()和fire()方法,所以命令周边的操作提取到了一个trait中,具体的命令引用这个trait即可,这个trait对应src/Console/Command.php。

1 命令:init

./envoy init 192.168.1.111

源码:

<?php

namespace Laravel\Envoy\Console;

use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Command\Command as SymfonyCommand;

class InitCommand extends SymfonyCommand
{
    use Command;

    /**
     * Configure the command options.
     *
     * @return void
     */
    protected function configure()
    {
        $this
            ->setName('init')
            ->setDescription('Create a new Envoy file in the current directory.')
            ->addArgument('host', InputArgument::REQUIRED, 'The host server to initialize with.');
    }

    /**
     * Execute the command.
     *
     * @return void
     */
    protected function fire()
    {
        if (file_exists(getcwd().'/Envoy.blade.php')) {
            $this->output->writeln('<error>Envoy file already exists!</error>');

            return;
        }

        file_put_contents(getcwd().'/Envoy.blade.php', "@servers(['web' => '".$this->input->getArgument('host')."'])

@task('deploy')
    cd /path/to/site
    git pull origin master
@endtask
");

        $this->output->writeln('<info>Envoy file created!</info>');
    }
}

方法configure()配置了命令名,需要什么参数,什么选项,命令注释等。fire()方法是具体的实现,命令的输入可用$this->input,命令的输出可用$this->output。

这里的envoy init只是往envoy所在的目录写入一个模板文件而已。

2 命令:tasks

./envoy tasks
Available tasks:
  foo
  deploy
  git
  composer

Available stories:
  story

实现:

protected function fire()
{
    $container = $this->loadTaskContainer();

    $this->listTasks($container);

    $this->output->writeln('');

    $this->listMacros($container);
}

protected function loadTaskContainer()
{
    if (! file_exists($envoyFile = getcwd().'/Envoy.blade.php')) {
        echo "Envoy.blade.php not found.\n";

        exit(1);
    }

    with($container = new TaskContainer)->load($envoyFile, new Compiler);

    return $container;
}

把Envoy.blade.php加载到taskContainer中,并指定了编译器。编译器对应src/Compiler.php,它把Envoy.blade.php这种Blade语法转换为PHP语法:

@servers(['web' => ['127.0.0.1']])

@setup
$now = new DateTime();
$environment = isset($env) ? $env : "testing";
@endsetup

@include('../../../vendor/autoload.php')

@task('foo', ['on' => 'web'])
    ls -la
@endtask

@task('deploy', ['on' => 'web'])
    cd site

    @if ($branch)
        git pull origin {{ $branch }}
    @endif

    php artisan migrate
@endtask

@story('story')
    git
    composer
@endstory

@task('git')
    git pull origin master
@endtask

@task('composer')
    composer install
@endtask

经过编译后得到:

<?php $branch = isset($branch) ? $branch : null; ?>
<?php $env = isset($env) ? $env : null; ?>
<?php $environment = isset($environment) ? $environment : null; ?>
<?php $now = isset($now) ? $now : null; ?>
<?php $__container->servers(['web' => ['127.0.0.1']]); ?>

<?php
$now = new DateTime();
$environment = isset($env) ? $env : "testing";
?>

 <?php require_once('../../../vendor/autoload.php'); ?>

<?php $__container->startTask('foo', ['on' => 'web']); ?>
    ls -la
<?php $__container->endTask(); ?>

<?php $__container->startTask('deploy', ['on' => 'web']); ?>
    cd site

    <?php if ($branch): ?>
        git pull origin <?php echo $branch; ?>

    <?php endif; ?>

    php artisan migrate
<?php $__container->endTask(); ?>

<?php $__container->startMacro('story'); ?>
    git
    composer
<?php $__container->endMacro(); ?>

<?php $__container->startTask('git'); ?>
    git pull origin master
<?php $__container->endTask(); ?>

<?php $__container->startTask('composer'); ?>
    composer install
<?php $__container->endTask(); ?>

然后taskContainer再把这个编译后的文件include进来, 实际执行容器里面的方法, 然后相关的数据就已经解析进入容器了,然后就调用相关的容器方法,比如取回任务等。

这里的tasks命令,实际就是调用容器的getTasks()方法。

3 命令:run

./envoy run task –continue –pretend –path= --conf=

选项—continue用来控制当执行一个任务失败后是否继续,这种情况发生在使用story指令来包装一组任务时,运行这个story实际是运行一组任务,默认不需要指定,表示不继续。
选项—pretend是伪装的意思,即把任务要运行的命令输出,并返回1(未执行成功)
选项—path用来指定任务定义的目录
选项—conf用来指定任务定义的文件

命令run的流程:
1 初始化容器
2 从容器中取回任务(脚本)
3 SSH登录服务器运行
4 运行任务执行后的回调

protected function fire()
{
    $container = $this->loadTaskContainer();

    $exitCode = 0;

    foreach ($this->getTasks($container) as $task) {
        $thisCode = $this->runTask($container, $task);

        if (0 !== $thisCode) {
            $exitCode = $thisCode;
        }

        if ($thisCode > 0 && ! $this->input->getOption('continue')) {
            $this->output->writeln('[<fg=red>✗</>] <fg=red>This task did not complete successfully on one of your servers.</>');

            break;
        }
    }

    foreach ($container->getFinishedCallbacks() as $callback) {
        call_user_func($callback);
    }

    return $exitCode;
}

这里的foreach是针对任务组(多个任务)的情况,然后调用runTask():

protected function runTask($container, $task)
{
    $macroOptions = $container->getMacroOptions($this->argument('task'));

    $confirm = $container->getTask($task, $macroOptions)->confirm;

    if ($confirm && ! $this->confirmTaskWithUser($task, $confirm)) {
        return;
    }

    if (($exitCode = $this->runTaskOverSSH($container->getTask($task, $macroOptions))) > 0) {
        foreach ($container->getErrorCallbacks() as $callback) {
            call_user_func($callback, $task);
        }

        return $exitCode;
    }

    foreach ($container->getAfterCallbacks() as $callback) {
        call_user_func($callback, $task);
    }
}

这里会返回一个Envoy/Task的对象实例传递到runTaskOverSSH()方法,Envoy/Task的对象是一个任务的简单封装,比如规定了任务在什么服务器上运行,是否并行运行等。

然后是runTaskOverSSH()方法:

protected function runTaskOverSSH(Task $task)
{
    // If the pretending option has been set, we'll simply dump the script out to the command
    // line so the developer can inspect it which is useful for just inspecting the script
    // before it is actually run against these servers. Allows checking for errors, etc.
    if ($this->pretending()) {
        echo $task->script.PHP_EOL;

        return 1;
    } else {
        return $this->passToRemoteProcessor($task);
    }
}

protected function passToRemoteProcessor(Task $task)
{
    return $this->getRemoteProcessor($task)->run($task, function ($type, $host, $line) {
        if (starts_with($line, 'Warning: Permanently added ')) {
            return;
        }

        $this->displayOutput($type, $host, $line);
    });
}
protected function getRemoteProcessor(Task $task)
{
    return $task->parallel ? new ParallelSSH : new SSH;
}

一个任务最终会放入到一个Envoy\SSH或ParallelSSH进程中执行(调用进程类的run()方法)。

进程类中,需要完成登录远程服务器,然后在远程服务器上执行脚本。登录服务器这个步骤,除非是本地的服务器(127.0.0.1),那么就需要认证,密码或私钥,这个是ssh外部命令完成。不过这里需要知道的是,SSH登录可以利用~/.ssh/config中定义的别名,比如针对这个别名定义了非22端口,那么就要解析这个文件,找到别名,然后ssh 别名。Laravel\Envoy\SSHConfigFile就解析这个config文件。

4 命令:ssh
这个命令基本是Bash中的ssh的包装,不过主机必须配置中定义过的主机:

@servers(['web' => 'root@192.168.1.111'])

那么就可以使用如下命令来登录:

./envoy ssh web

这个命令不过是找到web对应的机器root@192.168.1.111,然后ssh root@192.168.1.111而已:

protected function fire()
    {
        $host = $this->getServer($container = $this->loadTaskContainer());

        passthru('ssh '.($this->getConfiguredServer($host) ?: $host));
    }

这里的$host就是root@192.168.1.111,当然,这个机器可能在~/.ssh/config中定义了其它参数(比如非22端口),那么需要通过它找到别名,然后ssh别名。

Laravel Envoy最后一个功能就是命令执行完毕后,可以把结果发送给第三方,实际就是通知功能。

另外,一个任务可以异步发送到多台机器运行,但是不能多个任务同时运行。任务运行时可以指定continue参数,指定在任务失败时,是否继续,如果是并行运行的,这个参数似乎是无效的。另外,任务如果在多台机器上运行,只有全部成功才认为成功,可能发生前一个任务成功,后一个任务失败,这个时候需要设置回调做善后工作。

JDBC 参考

JDBC

下载JDBC驱动包(https://dev.mysql.com/downloads/connector/j/):
1 如果是一般Java项目,通过IDE添加包到项目
2 如果是Java Web项目,把包添加到WebContent/WEB-INF/lib中

在使用前,需要导入包(java.sql.*):

import java.sql.*;

<%@ page import="java.sql" %>

建立链接步骤:

import java.sql.*;

public class DB {

	public static void main(String[] args) {
                Connection conn = null;
                Statement stmt = null;
		try {
                        //Class.forName("oracle.jdbc.driver.OracleDriver");
			Class.forName("com.mysql.jdbc.Driver");
			String url = "jdbc:mysql://localhost:3306/test?useSSL=false&autoReconnect=true";
			String username = "root";
			String password = "root";
			
			conn = DriverManager.getConnection(url, username, password);
			if (conn != null) {
				System.out.println("数据库链接成功");
				stmt = conn.createStatement();
				ResultSet rs = stmt.executeQuery("SELECT * FROM tst");
				while (rs.next()) {
					System.out.println(rs.getInt("id"));
				}
				
				rs.close();
				stmt.close();
				conn.close();
			} else {
				System.out.println("数据库链接失败");
			}
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
	           try {
	    	       if (stmt != null) {
	                   stmt.close();
	    	       }
	           } catch (SQLException se2){
	           }
	
	           try {
	               if (conn!=null) {
	    	           conn.close();
	               }
	           } catch (SQLException se){
	               se.printStackTrace();
	           }
               }
	}

}

构建JDBC应用程序涉及以下六个步骤
导入包:需要包含包含数据库编程所需的JDBC类的包。 大多数情况下,使用import java.sql.*就足够了。
注册JDBC驱动程序:需要初始化驱动程序,以便可以打开与数据库的通信通道。
打开一个连接:需要使用DriverManager.getConnection()方法创建一个Connection对象,它表示与数据库的物理连接。
执行查询:需要使用类型为Statement的对象来构建和提交SQL语句到数据库。
从结果集中提取数据:需要使用相应的ResultSet.getXXX()方法从结果集中检索数据。
清理环境:需要明确地关闭所有数据库资源,而不依赖于JVM的垃圾收集。
注意:url后添加了useSSL=false参数,这个是因为高版本MySQL默认启用SSL进行链接,这里是明确不使用SSL。autoReconnect=true在丢失链接或链接未正确关闭时,可以重新建立链接。finally块用来保证资源正确关闭。

JDBC API提供以下接口和类
DriverManager:此类管理数据库驱动程序列表。 使用通信子协议将来自java应用程序的连接请求与适当的数据库驱动程序进行匹配。在JDBC下识别某个子协议的第一个驱动程序将用于建立数据库连接。
Driver:此接口处理与数据库服务器的通信。我们很少会直接与Driver对象进行交互。 但会使用DriverManager对象来管理这种类型的对象。 它还提取与使用Driver对象相关的信息。
Connection:此接口具有用于联系数据库的所有方法。 连接(Connection)对象表示通信上下文,即,与数据库的所有通信仅通过连接对象。
Statement:使用从此接口创建的对象将SQL语句提交到数据库。 除了执行存储过程之外,一些派生接口还接受参数。
ResultSet:在使用Statement对象执行SQL查询后,这些对象保存从数据库检索的数据。 它作为一个迭代器并可移动ResultSet对象查询的数据。
SQLException:此类处理数据库应用程序中发生的任何错误。

JDBC驱动程序类型(分1,2,3,4,4是纯Java实现)

JDBC数据类型
下表总结了当调用PreparedStatement或CallableStatement对象或ResultSet.updateXXX()方法的setXXX()方法时,将Java数据类型转换为的默认JDBC数据类型。

SQL类型 JDBC/Java类型 setXXX updateXXX
VARCHAR java.lang.String setString updateString
CHAR java.lang.String setString updateString
LONGVARCHAR java.lang.String setString updateString
BIT boolean setBoolean updateBoolean
NUMERIC java.math.BigDecimal setBigDecimal updateBigDecimal
TINYINT byte setByte updateByte
SMALLINT short setShort updateShort
INTEGER int setInt updateInt
BIGINT long setLong updateLong
REAL float setFloat updateFloat
FLOAT float setFloat updateFloat
DOUBLE double setDouble updateDouble
VARBINARY byte[ ] setBytes updateBytes
BINARY byte[ ] setBytes updateBytes
DATE java.sql.Date setDate updateDate
TIME java.sql.Time setTime updateTime
TIMESTAMP java.sql.Timestamp setTimestamp updateTimestamp
CLOB java.sql.Clob setClob updateClob
BLOB java.sql.Blob setBlob updateBlob
ARRAY java.sql.Array setARRAY updateARRAY
REF java.sql.Ref SetRef updateRef
STRUCT java.sql.Struct SetStruct updateStruct

处理NULL值(SQL使用NULL值和Java使用null是不同的概念)

Statement stmt = conn.createStatement( );
String sql = "SELECT id, first, last, age FROM Employees";
ResultSet rs = stmt.executeQuery(sql);

int id = rs.getInt(1);
if( rs.wasNull( ) ) {
   id = 0;
}

事务:

try{
   //Assume a valid connection object conn
   conn.setAutoCommit(false);
   Statement stmt = conn.createStatement();

   String SQL = "INSERT INTO Employees  " +
                "VALUES (106, 20, 'Rita', 'Tez')";
   stmt.executeUpdate(SQL);  
   //Submit a malformed SQL statement that breaks
   String SQL = "INSERTED IN Employees  " +
                "VALUES (107, 22, 'Sita', 'Singh')";
   stmt.executeUpdate(SQL);
   // If there is no error.
   conn.commit();
}catch(SQLException se){
   // If there is any error.
   conn.rollback();
}

Java Web 基础

JSP
1 了解JSP页面
2 指令标识
JSP指令标识的语法格式如下:

<%@ 指令名 属性1=“属性1” 属性2=“属性值2” ...%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

指令名称在JSP中包含page include和taglib。在一条指令中可设置多个属性,各属性使用逗号或空格分隔。

2.1 page指令
Page指令用于定义整个JSP页面的相关属性,这些属性在JSP被服务器解析成Servlet时会装换为相应的Java程序代码。Page指令包含属性有15个:
1 language属性

#当前固定值和默认值都为java
<%@ page language="java" %>

2 extends 属性
3 import 属性
导入类包:

<%@ page import="java.util.*" %>

4 pageEncoding 属性
定义JSP页面的编码格式:

<%@ page pageEncoding="UTF-8"%>

5 contentType 属性
该属性用于设置JSP页面的MIME类型和字符编码,流浪器会据此显示网页内容:

<%@ page contentType="text/html; charset=UTF-8" %>

6 session 属性
该属性指定JSP页面是否使用HTTP的session会话对象。属性值是boolean类型,默认值为true。

<%@ page session="false" %>

7 buffer 属性
该属性用于设置JSP的out输出对象使用的缓冲区,大小,默认为8KB,且单位只能使用KB。(out对象是JSP的内置对象之一)
8 autoFlush 顺序
该属性用于设置JSP页面缓存满时,是否自动刷新缓存。默认值为true。如果设置为false,则缓存被填满时将抛出异常。
9 isErrorPage属性
改过该属性可以将当前JSP页面设置成错误处理页面来处理另一个JSP页面的错误,也就是异常处理。
10 errorPage属性
该属性用于指定处理当前JSP页面异常错误的另一个JSP页面,指定的JSP错误处理页面必须设置isErrorPage属性为true。errorPage属性的属性值是一个url字符串。

<%@ page errorPage="error/loginErrorPage.jsp" %>

注:如果设置该属性,那么在web.xml文件中定义的任何错误页面都将被忽略。

2.2 include指令

<%@ include file="path" %>

该指令只有一个file属性,可以是绝对或相对路径。

2.3 taglib指令
在JSP文件中,可以通过taglib指令标识声明该页面中所随用的标签库,同时引用标签库,并指定标签的前缀。

<%@ taglib prefix="tagPrefix" url="tagURI" %>

3 脚本标识
3.1 JSP表达式
JSP表达式用于向页面中输出信息

<%= 表达式 %>

3.2 声明标识
格式:

<%! 声明变量或方法的代码 %>

服务器执行JSP页面时,会将JSP也爱你装换为Servlet类,在该类中会把使用JSP声明标识定义的变量和方法转换为类的成员变量和方法。

<%!
int number = 0;
int cout() {
    number++;
    return number;
}
%>

3.3 代码片段
代码片段在页面请求的处理期间被执行。

<% Java代码 %>

由于代码片段是在请求处理期间被执行的,所有的它的生命期是仅页面的,但是声明标识是对应到Servelet的,所有生命周期是整个应用的。

4 JSP注释
4.1 HTML中的注释
4.2 带有JSP表达式的注释
单行注释,多行注释,提示文档注释(会被Javadoc文档工具读取到)
4.3 隐藏注释

<%-- 注释内容 --%>

4.4 动态注释

<!-- <%= new Date() %> -->

5 动作标识
5.1 包含文件标识

<jsp:include page="url" flush="false|true">
    子动作标识<jsp:param>
</jsp:include>

JSP的动作标识用于向当前页面中包含其他的文件。被包含的文件可以是动态文件,也可以是静态文件(自动判断动态或静态,独立编译,把结果插入)。
子动作标识用来向被包含的文件传递参数。
指令include和动作标识有很大差别:
1)include指令通过file属性指定被包含的文件,并且file属性不支持任何表达式;动作标识通过page属性指定被包含的文件,并且page属性支持JSP表达式。
2) 使用include指令时,被包含的文件内容会原封不动地插入到包含页中,然后JSP编译器再将合并后的文件最终编译成一个Java文件;使用动作标识包含文件时,当该标识被执行时,程序会将请求转发(不是重定向)到被包含的页面,并将执行结果输出到浏览器中,然后返回包含页继续执行后面的代码
3)在应用include指令包含文件时,由于被包含的文件最终会生成一个文件,所以在被包含文件,包含文件不能有重名的变量或方法;而在引用动作标识包含文件时,由于每个文件是单独编译的,所以在被包含文件和包含文件中重名的变量和方法是不冲突的。

5.2 请求转发标识
通过动作标识可以将请求转发到其他的Web资源。执行请求转发后,当前页面将不再被执行,而是去执行该标识指定的目标页面。

<jsp:forward page="url" />
<jsp:forward page="url">
    子动作标识<jsp:param>
</jsp:forward>

5.3 传递参数标识
JSP的动作标识可以作为其他标识的子标识,用于为其他标识传递参数。格式:

<jsp:param name="参数名“ value="参数值" />

例子:

<jsp:forward page="modify.jsp">
    <jsp:param name="userId" value="7" />
</jsp:forward>

通过动作标识指定的参数,以键值对的形式加入到请求中。它的功能与在文件名后面加?参数名=参数值是相同的。

JSP内置对象

1 JSP内置对象
JSP中一共预定义了9个内置对象:request、response、session、application、out、pageContext、config、page和exception

2 request对象
2.1 访问请求参数

// 参数不存在,返回null
request.getParameter("id");

2.2 在作用域中管理属性
在进行请求转发时,需要把一些数据传递到转发后的页面进行处理。这时,就需要使用request对象的setAttribute()方法将数据保存到request范围内的变量中。

request.setAttribute(String name, Object object)

在将数据保存到request范围内的变量中后,可以通过request对象的getAttribute()方法获取该变量的值:

request.getAttribute(String name);

注意:由于getAttribute()方法的返回值为Object类型,所以需要调用其toString()方法,将其转换为字符串类型(如果设置的是字符串)。

2.3 获取cookie
通过cookie的getCookies()方法可以获取所有cookie对象的集合;通过cookie对象的getName()方法可以获取到指定名称的cookie;通过getValue()方法可获取到cookie对象的值;将一个cookie对象发送到客户端,使用response对象的addCookie()方法。

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page import="java.net.URLEncoder" %>   
<%@ page import="java.net.URLDecoder" %> 

<% 
Cookie[] cookies = request.getCookies();
if (cookies != null) {
    for (int i = 0; i < cookies.length; i++) {
        out.println(cookies[i].getName() + " = " +  URLDecoder.decode(cookies[i].getValue()));
    }
}

String cookieValue = URLEncoder.encode("饼干");
Cookie cookie = new Cookie("ifeeline", cookieValue);
cookie.setMaxAge(3600);
response.addCookie(cookie);
%>

在向cookie中保存的信息中,如果包括中文,需要调用java.net.URLEncode类的encode()方法将值进行编码,在取时需要用java.net.URLDecoder类的decode()方法进行解码。
2.4 解决中文乱码
发送请求的参数值编码和服务端编码不一致。
2.5 获取客户端信息

br />传送数据的方法(GET POST)<%= request.getMethod() %>
<br />协议名称<%= request.getProtocol() %>
<br />请求地址(不包括请求参数)<%= request.getRequestURL() %>
<br />客户端IP<%= request.getRemoteAddr() %>
<br />获取服务器端口号<%= request.getServerPort() %>
<br />获取服务器名称<%= request.getServerName() %>
<br />获取客户端主机名<%= request.getRemoteHost() %>
<br /><%= request.getServletPath() %>
<br />请求头(host)<%= request.getHeader("host") %>			
<br />请求头(user-agent)<%= request.getHeader("user-agent") %>
<br />当前请求文件的绝对路径<%= request.getRealPath("index.jsp") %>

2.6 显示国际化信息
流浪器通过accept-language的HTTP报头向Web服务器指明它所使用的本地语言。request对象中的getLocale()和getLocales()方法获取这部分信息。

<%
java.util.Locale locale = request.getLocale();
String str = "";
if (locale.equals(java.util.Locale.US)) {

}
if (locale.equals(java.util.Locale.CHINA)) {

}

3 response对象
3.1 重定向网页

response.sendRedirect(String path);

3.2 处理HTTP响应头
1)禁用缓存

response.setHeader("Cache-Control", "no-store");
response.setDateHeader("Expires", 0);

2) 设置页面自动刷新

response.setHeader("refresh", "10");

3) 定时跳转网页

response.setHeader("refresh", "5;URL=login.jsp");

3.3 设置输出缓冲

flushBuffer() 强制将缓冲区输出到客户端
getBufferSize() 获取缓冲区大小
setBufferSize(int size) 设置缓冲区的大小
reset() 清除缓冲区的内容
isCommitted() 检测服务端是否已经把数据写入到客户端

4 session对象
4.1 创建及获取客户的会话

session.setAttribute(String name, Object obj)
session.getAttribute(String name);

注:getAttribute()方法返回值是Object类型。如果需要赋值给String变量,需要调用toString()方法(或强制类型转换)。

4.2 从会话中移动指定的绑定对象

removeAttribute(String name)

4.3 销毁session

session.invalidate()

销毁session后,将不可以再使用该sessiond对象。如果再调用session对象的任何方法,抛Session already invalidated异常。

4.4 会话超时的管理

session.getLastAccessedTime()
session.getMaxInactiveInterval() 两个请求允许的最大时间间隔(有效时长)
session.setMaxInactiveInterval() 设置时长

4.5 session对象的应用

5 application对象
5.1 访问应用程序初始化参数
在web.xml中通过标记配置应用程序初始化参数。

<context-param>
    <param-name>url</param-name>
    <param-value>jdbc:mysql://127.0.0.1:3306/test</param-value>
</context-param>

提供了两种访问应用程序初始化参数的方法:

application.getInitParameter(String name)
application.getAttributeNames() // 返回所有

实例:

<%@ page import="java.util.*" %>
<%
Enumeration enema = application.getInitParameterNames();
while (enema.hasMoreElements() ) {
    String name = (String)enema.nextElement();
    String value = application.getInitParameter(anme);
}
%>

5.2 管理应用程序环境属性

getAttributeNames()
getAttribute(String name);
setAttribute(String name, Object obj)
removeAttribute(String name)

6 out对象
6.1 向客户端输出数据
6.2 管理响应缓冲

clear() 清除缓冲区的内容(如响应已提交,产生IOException异常)
clearBuffer() 清除当前缓冲区中的内容
flush() 
isAutoFlush()
getBufferSize()

7 其它内置对象
7.1 获取会话范围的pageContent对象

forward(java.lang.String relativeUrlpath)

7.2 读取web.xml配置信息的config对象
主要用于取得服务器的配置信息。通过pageContext对象的getServletConfig()方法可以获取一个config对象。
7.3 应答或请求的page对象
page对象代表JSP本身,只有在JSP页面内才是合法的。page对象本质上是包含当前Servlet接口引用的变量,可以看作是this关键字的别名。
7.4 获取异常信息的exception对象
只有在page指令中设置为isErrorPage属性值为true的页面中才可以被使用。如果在JSP页面中出现没有捕捉到的异常,就会生成exception对象,并把exception对象传送到page指令中设定的错误页面中。

JavaBean
1 JavaBean介绍
2 JavaBean的应用
2.1 获取JavaBean属性信息
在JavaBean对象中,为了防止外部直接对JavaBean属性的调用,通常将JavaBean中的属性设置为私有,单需要为其提供公共的方法方法。

<jsp:useBean id="produce" class="com.ifeeline.bean.Product"></jsp:useBean>
<jsp:getProperty property="count" name="produce" />

实际是调用produce对象的getCount()方法。会自动实例化一个对象,所以Bean类型需要一个无参的构造函数。操作属性时需要提供getXxx方法。

2.2 对JavaBean属性赋值
如果对JavaBean对象的属性提供了setXxx方法,在JSP页面中可通过对其进行赋值。

2.3 在JSP页面中应用JavaBean
通过JSP动作标签 来实现对JavaBean对象的操作。将JavaBean对象应用到JSP页面中,JavaBean的生命周期可以自行进行设置,它存在于4种范围内:page,request,session和application。默认作用于page。
<
/jsp:useBean>

Servlet技术
1 基础
Servlet对象主要封装了对HTTP请求的处理,并且它的运行需要Servlet容器的支持。
1.1 Servlet结构体系
抽象类GenericServlet实现Servlet(javax.servlet)、ServletConfig(javax.servlet)、Serializable接口(java.io),HttpServlet继续GenericServlet。

1.2 Servlet技术特点

1.3 Servlet与JSP的区别

1.4 Servlet代码结构

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TestServlet extends HttpServlet {
    public void init() throws ServletException {}
    public void destroy() {
	super.destroy();
    }

}

2 Servlet API编程常用接口和类
3 Servlet开发
3.1 Servlet创建
3.2 Servlet配置
要使Servlet对象正常地运行,需要进行适当的配置,以告知Web容器哪一个请求调用哪一个Servlet对象处理,对Servlet起到一个注册的作用。Servlet的配置包含在web.xml文件中,通过如下两步设置:
1)声明Servlet对象

<servlet>
    <servlet-name>TestServlet</servlet-name>
    <servlet-class>com.ifeeline.TestServlet</servlet-class>
</servlet>

2)映射Servlet

<servlet-mapping>
    <servlet-name>TestServlet</servlet-name>
    <url-pattem>/TestServlet</url-pattem>
</servlet-mapping>

Servlet过滤器与监听器
1 Servlet过滤器
1.1 过滤器
1.2 过滤器核心对象
过滤器对象放置在javax.servlet包中,名称为Filter,相关的有FilterConfig和FilterChain。
1.3 过滤器创建与配置
1.4 字符编码过滤器

2 Servlet监听器
2.1 Servlet监听器
2.2 Servlet监听器的原理
2.3 Servlet上下文监听
2.4 HTTP会话监听
2.5 Servlet请求监听

3 Servlet3.0 新特征

Java Web数据库操作
1 JDBC
1.1 JDBC
1.2 JDBC链接数据库的过程
下载驱动https://dev.mysql.com/downloads/connector/j/,得到mysql-connector-java-5.1.45-bin.jar,放入WEB-INF/lib中,重启应用:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%
try {
	Class.forName("com.mysql.jdbc.Driver");
	String url = "jdbc:mysql://localhost:3306/test";
	String username = "root";
	String password = "root";
	
	Connection conn = DriverManager.getConnection(url, username, password);
	if (conn != null) {
		out.println("数据库链接成功");
		Statement stmt = conn.createStatement();
		ResultSet rs = stmt.executeQuery("SELECT * FROM tst");
		while (rs.next()) {
			out.println(rs.getInt("id"));
		}

                PreparedStatement ps = conn.prepareStatement("INSERT INTO tst (name, price) values(?, ?)");
		ps.setString(1, "xxxxxx");
		ps.setDouble(2, 2.22);
		int row = ps.executeUpdate();
		ps.close();

		rs.close();
		stmt.close();
		conn.close();
	} else {
		out.println("数据库链接失败");
	}
} catch (ClassNotFoundException e) {
	e.printStackTrace();
} catch (SQLException e) {
	e.printStackTrace();
}
%>

2 JDBC API
2.1 Connection接口
Connection接口位于java.sql包中。

void close() throws SQLException
void commit() throws SQLException
Statement createStatement() throws SQLException
boolean getAutoCommit() throws SQLException
DatabaseMetaData getMetaData() throws SQLException
int getTransactionIsolation() throws SQLException
boolean isClosed() throws SQLException
boolean isReadOnly() throws SQLException
PreparedStatement PreparedStatement(String sql) throws SQLException
void releaseSavepoint(Savepoint savepoint) throws SQLException
void rollback() throws SQLException
void rollback(Savepoint savepoint) throws SQLException
void setAutoCommit(boolean autoCommit) throws SQLException
void setReadOny(boolean readOnly) throws SQLException
Savepoint setSavepoint() throws SQLException
void setSavepoint(String name) throws SQLException
void setTransactionIsolation(int level) throws SQLException

2.2 DriverManager类

public static Connection getConnection(String url) throws SQLException
void commit() throws SQLException

2.3 Statement接口

void close() throws SQLException
boolean execute(String sql) throws SQLException(执行指定的SQL语句)
ResultSet executeQuery(String sql) throws SQLException(执行查询)
int executeUpdate(String sql) throws SQLException(insert update delete, 返回影响的行数)
boolean isClosed() throws SQLException

2.4 PreparedStatement接口
PreparedStatement接口继承于Statement接口。

2.5 ResultSet接口

EL(表达式语言)
1 EL
1.1 EL的基本语法
${expression}
1.2 EL的特点
EL中会自动进行类型装换
EL不仅可访问一般变量,还可访问JavaBean中的属性已经嵌套属性和集合对象

2 禁用EL
3 保留的关键字
4 EL的运算符及优先级
4.1 通过EL访问数据
通过EL提供的[]和.运算符可以访问数据。[]和.运算符等价。
4.2 EL中的算术运算
4.3 EL中判断对象是否为空
${empty expression}
${not empty expression}
4.4 在EL中进行逻辑关系运算
4.5 在EL中进行条件运算

5 EL的隐含对象
5.1 页面上下文对象
页面上下文对象为pageContext,用于访问JSP内置对象(如request,response,out,session,exception和page等,但不能用于获取application,config和pageContext对象)和servletContext。

6 定义和使用EL函数


JSTL标签

Java Web 运行环境

2004年10月 JDK 1.5(Java SE 5)
2006年12月 JDK 1.6(Java SE 6)
2011年07月 JDK 1.7(Java SE 7) Oracle时代
2014年03月 JDK 1.8(Java SE 8)
2017年09月 JDK 1.9(Java SE 9)

JVM是一个虚拟机,只认识字节码。JRE包含虚拟机,还包含运行Java程序的其它环境支持。JDK也包含了JRE,除此还包含了一系列工具,比如编译Java程序的javac。

JDK下载地址:http://www.oracle.com/technetwork/java/javase/downloads/index.html

Linux 和 Windows区分32位和64位。Mac OS X不区分。Windows下提供了exe安装包。Mac OS X下提供DMG安装包。Linux提供了RPM和GZ压缩包。

需要明确知道安装路径:
Windows上一般就是C:\Program Files\Java\jdk1.8.0_version,一般不应该放入到带空格的路径中。
Linux只要解压tar.gz文件到某个位置,如果使用RPM包安装,可以通过rpm -ql来确定安装位置(一般安装在/usr/java/jdk1.8.0,不需要单独设置环境变量,RPM安装做了一系列软连接)。
Mac OS X中,安装路径:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/

安装完成后可设置环境变量,Linux中一般可以往~/.bash_profile中添加:
export PATH=”/usr/local/java/bin:$PATH”
Windows下设置Path环境变量。

注:除非是下载源代码,否则不需要再设置环境变量。


以上是Mac中的安装,安装完毕:

cd /Library/Java/JavaVirtualMachines/

ls
jdk-9.0.4.jdk		jdk1.8.0_144.jdk

which javac
/usr/bin/javac

javac --version
javac 9.0.4

当前安装了两个版本。可以看到,9之前的命名是1.8.0,从9开始就变成了9。

确认版本号:

which javac
javac -version

用于编译Java程序所使用的javac命令是使用Java编写的,这个类是lib路径下tools.jar文件中sun
tools\javac路径下的Main类,JDK的bin路径下的javac命令实际仅是包装了这个Java类,bin路径下大部分命令都是包装了tools.jar文件里的工具类。

库源代码在JDK中以src.zip的形式发布。文档:http://docs.oracle.com/javase/8/

Tomcat安装。

在Mac下可用brew install tomcat,也可以到https://tomcat.apache.org手动下载安装。Tomcat本身是一个Java软件,可以运行多个实例。

Tomcat目录:

bin:存放tomcat命令
conf:存放tomcat配置信息,里面的server.xml文件是核心的配置文件
lib:支持tomcat软件运行的jar包和技术支持包(如servlet和jsp)
logs:运行时的日志信息
temp:临时目录
webapps:共享资源文件和web应用目录
work:tomcat的运行目录.jsp运行时产生的临时文件就存放在这里

启动关闭:

catalina start
catalina stop

启动后:

/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/bin/java 
-Dcatalina.base=/Users/vfeelit/eclipse-workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp1 
-Dcatalina.home=/usr/local/Cellar/tomcat/9.0.6/libexec 
-Dwtp.deploy=/Users/vfeelit/eclipse-workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp1/wtpwebapps 
-Djava.endorsed.dirs=/usr/local/Cellar/tomcat/9.0.6/libexec/endorsed 
-Dfile.encoding=UTF-8 

-classpath 
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/resources.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/rt.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/jsse.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/jce.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/charsets.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/jfr.jar:

/usr/local/Cellar/tomcat/9.0.6/libexec/bin/bootstrap.jar:
/usr/local/Cellar/tomcat/9.0.6/libexec/bin/tomcat-juli.jar:

/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/lib/tools.jar org.apache.catalina.startup.Bootstrap start

把Tomcat中libexec/bin/中的bootstrap.jar和tomcat-juli.jar在classpath中出现。启动Tomcat不过是启动一个Java虚拟机实例而已。

Composer中replace属性的作用

原始解释:
“Lists packages that are replaced by this package. This allows you to fork a package, publish it under a different name with its own version numbers, while packages requiring the original package continue to work with your fork because it replaces the original package.”

列出被当前包替换的包。这允许你fork一个包,并以不同的名称和它自己的版本号进行发布,由于替换了原始的包,当包依赖原始包时可以使用fork包继续工作。

实际上,第一句话已经解释清楚了,就是当前包替换哪个(或哪些)包。比如:

{
“name": "b/b",
"replace": {
    "a/a": "x.y.z"
}
}

就是b/b这个包替换了a/a这个对应了x.y.z版本的包。如果b/b包中的replace列出了多个包,那么它将替换多个包。在Composer解决依赖的过程中,如果遇到了replace,那么原始的包会被移除,所以它的应用场景非常清晰,就是原始包停止维护了,但是你的项目或包直接或间接依赖一个原始的包,为了可以修复原始包的bug,你可以fork这个包,并在这个包中声明要替换原始包,然后在你的应用或包中依赖这个你fork的包,这样就可以全局替换原始包(原始包被移除)。

由于是包替换,很明显也可能引入安全问题,比如某个包被替换,替换的包中包含恶意代码。

关于replace的第二段解释:
”This is also useful for packages that contain sub-packages, for example the main symfony/symfony package contains all the Symfony Components which are also available as individual packages. If you require the main package it will automatically fulfill any requirement of one of the individual components, since it replaces them.“

这里描述的就是现代框架的组织方式。一个框架(或一个大组件),是很多子包组成的,每个子包都可以单独使用,通常每个子包都会以只读的方式fork到另一个包名,当单独使用时,可以依赖这个只读的包,当依赖整个框架时,框架中声明了替换,则可以移除单独依赖时下载的包。

比如laravel/framework是由很多包组成的,每个包都以只读的方式fork到另一个包名称:

// https://github.com/laravel/framework
"replace": {
        "illuminate/auth": "self.version",
        "illuminate/broadcasting": "self.version",
        "illuminate/bus": "self.version",
        "illuminate/cache": "self.version",
        "illuminate/config": "self.version",
        "illuminate/console": "self.version",
        "illuminate/container": "self.version"
}

这个包包括了子包illuminate/container,而它被只读方式fork了一份到https://github.com/illuminate/container,所以当要仅用这个组件时,可以composer require illuminate/container即可。在依赖了illuminate/container后,如何由依赖laravel/framework,这个时候会引起命名冲突,但是laravel/framework中声明了illuminate/container这个包被laravel/framework替换,所以在依赖laravel/framework后,illuminate/container独立的下载将被移除,从而解决了名称冲突。

第一个用法,提供了一个全局包替换的机会。第二个用法,提供了一个现代化的框架组织方式。

Laravel 容器源码研究

<?php
//error_reporting(0);

include "vendor/autoload.php";

class A
{
    public $b = null;

    public $p = 0;

    public function __construct($b, $p)
    {
        $this->b = $b;
        $this->p = $p;
    }

    public function test()
    {
        echo "A-------\n";
        var_dump($this->p);
        $this->b->test();
    }
}

class B
{
    public $c = null;

    public $o = 0;

    public function __construct(C $c, $o = 0)
    {
        $this->c = $c;
        $this->o = $o;
    }

    public function test()
    {
        echo "B-------\n";
        if ($this->o) {
            echo 'Great 0-----';
        }
        $this->c->test();
    }
}

class C
{
    public function test()
    {
        echo "C-------\n";
    }
}

$container = new \Illuminate\Container\Container();
$p = 10101000;

// A类构造函数依赖形参$p(没有默认值),需要传递: 通过when()方法指定
$b = $container->make(B::class, ['o' => 1]);
$container->when(A::class)->needs('$p')->give($p);
$container->when(A::class)->needs('$b')->give($b);
//$container->when(A::class)->needs(B::class)->give(function () use ($b) {
//    return $b;
//});
$a = $container->make(A::class);

// A类构造函数依赖形参$p(没有默认值),需要传递:通过make()方法的第二参数传递
//$a = $container->make(A::class, ['p' => $p]);
$a->test();

dd((array)$container);

一、对象构建:

    public function make($abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }

方法make只是resolve()方法的包装而已(注意:make方法是公共的,而resolve是受保护的):

    protected function resolve($abstract, $parameters = [])
    {
	// 如果是别名,取回原来的名称:比如aa是a的别名,传递aa进来,返回a
	// 传递a进来,直接返回a
        $abstract = $this->getAlias($abstract);

        $needsContextualBuild = ! empty($parameters) || ! is_null(
            $this->getContextualConcrete($abstract)
        );

        // 在instances中,并且没有上下文对象,直接返回
        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }

	// 把传递进来的参数进行临时保存(入栈)
        $this->with[] = $parameters;

        $concrete = $this->getConcrete($abstract);

        // 递归构建对象
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        // 应用对象装饰
        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

        // 如果是share类型,并且没有上下文依赖,就存入instances:并不是设置为share就一定进入instances
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }

	// 回调
        $this->fireResolvingCallbacks($abstract, $object);

        // 标记已经取回
        $this->resolved[$abstract] = true;

	// 出栈
        array_pop($this->with);

        return $object;
    }

类目传递了A,这时$abstract是A, $parameters是[‘p’ => $p],$abstract经过getAlias后没有发送变化(不是别名),由于参数不是空,$needsContextualBuild为true; $this->getConcrete($abstract)后得到的$concrete是$abstract本身(都是A),然后判断是否可以构建:

    protected function isBuildable($concrete, $abstract)
    {
        return $concrete === $abstract || $concrete instanceof Closure;
    }

是否可以构建,条件是$concrete和$abstract相等,或者$concrete是一个闭包函数。所以这里会进入build()方法:

    public function build($concrete)
    {
        // 如果是闭包,调用闭包函数:第一参数是容器,第二参数是外部传递进来的参数
        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }
	// 不是闭包就利用反射:$concrete这里应该是类名(或类对象)
        $reflector = new ReflectionClass($concrete);

        // 如果不可实例化,抛异常
        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }

	// 由于可能会依赖其它对象,这里先把当前类目入栈
        $this->buildStack[] = $concrete;
	// 取回构造函数
        $constructor = $reflector->getConstructor();

        // 构造函数为空,直接实例化
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }
	// 否则就是指定了构造函数,那么解析这个构造函数,取回参数
        $dependencies = $constructor->getParameters();

        // 根据构造函数的参数,实例化依赖,这里可能会产生递归
	// 另外,如果外部有传递参数进来,会优先使用
        $instances = $this->resolveDependencies(
            $dependencies
        );
	// 出栈
        array_pop($this->buildStack);

	// 把实例对象传递构造函数生成对象
        return $reflector->newInstanceArgs($instances);
    }

这个构建的过程,利用反射,如果依赖其它对象,则递归构建依赖。 这里面用到了两个栈,with是参数堆栈,每次构建从其中取出参数,填充到构造函数,另一个是buildStack。这里可以看到A传递进来,它不是闭包,那么就开始了下面的反射构建过程,A是可以实例化的,有构造函数,那么会进入resolveDependencies()递归解决依赖:

    protected function resolveDependencies(array $dependencies)
    {
        $results = [];

        foreach ($dependencies as $dependency) {
            if ($this->hasParameterOverride($dependency)) {
                $results[] = $this->getParameterOverride($dependency);

                continue;
            }

            $results[] = is_null($dependency->getClass())
                            ? $this->resolvePrimitive($dependency)
                            : $this->resolveClass($dependency);
        }

        return $results;
    }

这里看明白如下几个函数:

    protected function hasParameterOverride($dependency)
    {
        return array_key_exists(
            $dependency->name, $this->getLastParameterOverride()
        );
    }

    protected function getParameterOverride($dependency)
    {
        return $this->getLastParameterOverride()[$dependency->name];
    }

    protected function getLastParameterOverride()
    {
        return count($this->with) ? end($this->with) : [];
    }

方法getLastParameterOverride取回with堆栈的头元素,就是传递进来的参数,方法hasParameterOverride就是判断传递进来的参数有没有对应名称的变量,方法getParameterOverride就是取到传递进来的值。在这,传递进来的参数是[‘p’ => $p], 那么在构建A时,取到形参p,p出现在了[‘p’ => $p]中,所以A构造函数的$p就等于传递进来的变量$p。如果没有对应的参数,就检查是否有指定上下文参数,有则使用,没有,如果是类就去实例类(可能递归),如果是原始变量,就使用默认值。

由于A构造函数需要B对象,而B对象又需要C对象,所以会进入递归,最终返回的对象传递到A构造函数。从这个过程可见,容器的make方法,可以自动构建对象。如果没有构造函数,或构造函数没有依赖其它对象,直接new即可。到这里,还有问题问解决,当构建A时,会去构建B,如果希望使用一个现成的B对象,这个需要依靠上下文绑定来解决。

二、上下文绑定:

//当解析A::class,如果需要B::class类型对象,那么对应的对象是$b。
$container->when(A::class)->needs(B::class)->give(function () use ($b) {
    return $b;
});
//当解析A::class,如果需要形参是$b时,那么对应的值是$b。
$container->when(A::class)->needs('$b')->give($b);

注意语法,当需要的是类类型时,需要给一个闭包函数,否则直接指定值。另外,如果构造函数依赖的是B $b,这个时候直接给值的语法可能是无作用的,因为如果有类型限定,并且类型是类类型,那么总是根据类来实例化对象,这个过程会取回闭包执行返回对象。

这个用法实际产生如下数组:

$this->contextual['A']['B'] = function () use ($b) {
    return $b;
};
$this->contextual['A']['$b'] = $b;

当A实例化是,遇到了需要实例化B,那么首先判断是否存在$this->contextual[‘A’][‘B’],如果存在直接用,否则就去构造。当遇到形参$b时,就去判断$this->contextual[‘A’][‘$b’]是否存在,存在直接用,否则利用反射API去使用默认值。

容器的when()方法不过是一个语法糖,最终会调用addContextualBinding()方法来设置上下绑定:

    public function addContextualBinding($concrete, $abstract, $implementation)
    {
        $this->contextual[$concrete][$this->getAlias($abstract)] = $implementation;
    }

可以这样用:

$container->addContextualBinding(A::class, B::class, function () use ($b) {
    return $b;
});
$container->addContextualBinding(A::class, '$b', $b);

这个上下文绑定,跟make()时传递的第二参数作用非常类似,不过make时传递的参数需要和构造函数的形参名称相同,并且总是优先使用的,比如:

$container->addContextualBinding(A::class, '$b', $b);

$container->make(A::class, ['$b' => $b]);

这里的设置了上下文,也传递了第二参数,那么make时传递的总是优先被使用。一旦设置了上下文,或make时传递了参数,那么生成的对象都是独立(不共享,不会压入$this->instances数组),哪怕设置的绑定是共享的,因为跟上下文相关的对象,是特定的。

上下文设置或构建时的参数传递严格意义上说并不是容器需要实现的内容。这里显然是为了能便利的构建依赖而引入的便利方法。

三、对象构建时的回调:
对象构建之后,可以设置回调,可以全局设置回调(每构建一个对象都调用),或者设置在构建某个类型的对象时才执行回调:

$container = new \Illuminate\Container\Container();
$callBack = function () {
    echo "Callback -------\n";
};

$globalAfterCallBack = function () {
    echo "Global After Callback -------\n";
};

$afterCallBack = function () {
    echo "After Callback -------\n";
};
$container->resolving($globalCallBack);
$container->resolving('a', $callBack);
$container->afterResolving($globalAfterCallBack);
$container->afterResolving('a', $afterCallBack);

四、对象装饰器:
对象构建后,需要执行对象的某个方法,或者运行某个逻辑来扩展一下这个对象,这个就是对象的装饰。在对象生成后,可以使用extend来装饰:

$container->extend(Service::class, function($service) {
    return new DecoratedService($service);
});

五、绑定
在理解了以上基本内容后,再来看看绑定,应该就很好理解了。

// 直接绑定一个类,可以这样做,除了类实例化会延后外,没有额外好处,一般应该避免这个用法
// 在需要是直接make即可
$container->bind(A::class);

// 设置服务,当make(‘a’)时,会实时生成一个A对象
$container->bind(‘a’, A::class);

// 绑定接口,AI实现了I的接口
$container->bind(‘I’, AI::class);

// 设置服务,指定闭包函数,多见
$container->bind(‘events’, function () {});

主要到以上用法尽管都是合法的,但是绑定闭包这种用法比较多见。另外,bing的第三参数控制这个bind是否是共享的,也就是生成实例是否是单例的,bind方法默认是不共享的,有一个快捷方法singleton()可以直接设置共享绑定。

如果绑定是共享的,生成的实例会压入instances数组,表示这些是共享对象,所以有一个方法,可以往这个数组中直接压入内容进行全局共享:instance($key, $instance)。

接下来看看具体过程:

    public function bind($abstract, $concrete = null, $shared = false)
    {
        $this->dropStaleInstances($abstract);

        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        if (! $concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');

        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }

简单来说就是对一个类型设置绑定,第二参数是要绑定的内容,第三参数设置是否共享,它决定了对象是否是单例对象。首先调用的$this->dropStaleInstances($abstract),对应的方法实际就是清空该类型对应的绑定和对应的实例:

    protected function dropStaleInstances($abstract)
    {
        unset($this->instances[$abstract], $this->aliases[$abstract]);
    }

如果第二参数是null(没有传递需要绑定的内容),那么绑定的内容就是类型本身。如果绑定的内容如果不是一个闭包函数,那么就调用getClosure()把内容变成一个闭包函数:

    protected function getClosure($abstract, $concrete)
    {
        return function ($container, $parameters = []) use ($abstract, $concrete) {
            if ($abstract == $concrete) {
                return $container->build($concrete);
            }

            return $container->make($concrete, $parameters);
        };
    }

这个getClosure()返回的是一个闭包函数,返回的这个闭包函数接受两个参数($container, $parameters = []),如果类型和内容一样,就直接build,否则就是make一个对象,总之现在知道其返回的是function ($container, $parameters = [])即可。

接下来按照一个统一的格式设置绑定:

$this->bindings[$abstract] = compact('concrete', 'shared');

if ($this->resolved($abstract)) {
    $this->rebound($abstract);
}

// 举例来说
$this->bindings[‘a’] = [
    ‘concrete’ => function ($container, $parameters = []) {},
    ‘shared’ => false
];

那么在a实例被生成时,concrete对应的闭包函数会被执行,其中会根据shared来决定是否设置共享实例。resolved()方法用来判断绑定是否已经生成了实例,如果生成了实例,调用rebound()方法重新生成一次(实际是调用make方法,不过,在重写生成对象时,可以对特定类型设置回调,这个方法会执行回调)。

绑定的其它情况分析:
第二参数只要不是闭包函数,那么总会调用getClosure()方法封装为一个闭包函数:
1 当第二参数为空时,把第一参数赋值给第二参数,调用getClosure()方法

class A {}

$container->bind(A::class);
// $abstract = $concrete = A::class
$container->bind['A'] = function ($container, $parameters = []) use ($abstract, $concrete) {
     if ($abstract == $concrete) {
         return $container->build($concrete);
     }

     return $container->make($concrete, $parameters);
};
// 此时获取该服务跟直接new基本一样
// 不一样的是实例生成时,如果构造函数依赖容器对象,可以自动注入

2 当是一个对象时,调用getClosure()方法

class A {}

$container->bind('aa', new A());
// $abstract = 'aa'; $concrete = new A();
$container->bind['aa'] = function ($container, $parameters = []) use ($abstract, $concrete) {
     if ($abstract == $concrete) {
         return $container->build($concrete);
     }

     return $container->make($concrete, $parameters);
};
// 在实例化时会运行$container->make('aa', []), ‘aa’对应的闭包函数被运行,该函数内再次运行$container->make($concrete, $parameters);
// 此时的$concrete是一个对象,PHP的isset($this->aliases[$concrete])会报警告,但不会影响程序运行
// 第二次运行的make最终会到达build函数,这是的build不过是根据$concrete(此时是一个对象)并应用反射,重新生成一个对象而已

所以这两种用法都应该避免。相反,在使用bind时,应该总是使用闭包。

另外,第二参数是闭包时,这个闭包函数在定义参数时注意:

$container->bind('aa', function ($a, $b, $c) {});

如果调用make时,将会报错。因为闭包调用时总是如此格式:

return $concrete($this, $this->getLastParameterOverride());

闭包可以不定义参数,以上的用法不会发生错误。如果定义了参数,第一个参数总是代表容器实例本身,第二个参数是一个数组(可不定义)用来从外传入数据到闭包中。如果多于2个参数就发生错误。如果定义了第二参数,那么在make对象时,就可以传递数据进去(注:如果是单例实例,一旦生成后不会再改变,除非强制rebund,所以不能期望通过传递参数改变单例对象的状态)

最终提炼一下公共方法:

public function when($concrete)

// 服务是否已经设置
public function bound($abstract)

// 方法bound的别名
public function has($id)

// 服务是否已经实例化
public function resolved($abstract)

// 服务是否是共享的(单例)
public function isShared($abstract)

// 名称是否是别名
public function isAlias($name)

// 服务绑定
public function bind($abstract, $concrete = null, $shared = false)

public function hasMethodBinding($method)
public function bindMethod($method, $callback)
public function callMethodBinding($method, $instance)

// 上下文绑定
public function addContextualBinding($concrete, $abstract, $implementation)

// 条件绑定,bind方法的再封装
public function bindIf($abstract, $concrete = null, $shared = false)

// 单例绑定
public function singleton($abstract, $concrete = null)

// 服务器装饰器
public function extend($abstract, Closure $closure)

// 直接注入内容到容器
public function instance($abstract, $instance)

public function tag($abstracts, $tags)
public function tagged($tag)

// 服务别名设置
public function alias($abstract, $alias)

// 绑定回调设置
public function rebinding($abstract, Closure $callback)

public function refresh($abstract, $target, $method)
public function wrap(Closure $callback, array $parameters = [])
public function call($callback, array $parameters = [], $defaultMethod = null)

// 用一个闭包封装make
public function factory($abstract)

// 方法make的另一种形式
public function makeWith($abstract, array $parameters = [])

// 构建服务
public function make($abstract, array $parameters = [])
// 
public function get($id)

// 构建服务
public function build($concrete)

public function resolving($abstract, Closure $callback = null)
public function afterResolving($abstract, Closure $callback = null)

public function getBindings()
public function getAlias($abstract)
public function forgetExtenders($abstract)
public function forgetInstance($abstract)
public function forgetInstances()
public function flush()

实际常用的就是几个方法。