这篇文章主要介绍“Laravel容器、控制反转和依赖注入实例分析”,在日常操作中,相信很多人在Laravel容器、控制反转和依赖注入实例分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Laravel容器、控制反转和依赖注入实例分析”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
随着现在应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,耦合程度越来越高,经常会出现对象之间多重依赖的情况。对于如此庞大复杂的应用,任何修改都可能会牵一发而动全身,这就为应用的后期维护造成了很多困扰。
为了解决对象之间耦合度高的问题,控制反转(IoC)的思想也随之诞生。所谓控制反转,是面向对象编程中的一种设计原则,其目的是为了降低代码之间的耦合程度。在 Laravel 中,控制反转是通过依赖注入(DI)的方式实现的。
控制反转的基本思想是借助 IoC 容器实现对象之间的依赖关系的解耦。引入 IoC 容器之后,所有对象的控制权都上交给 IoC 容器,IoC 容器成了整个系统的核心,把所有对象粘合在一起发挥作用。Laravel 中的容器即起到了这个作用。
所谓容器,在 Laravel 中指的是 \Illuminate\Foundation\Application 对象,Laravel 框架在启动时即创建了该对象。
# public/index.php
$app = require_once __DIR__.'/../bootstrap/app.php';
# bootstrap/app.php
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
在创建容器的过程中,Laravel 还会对容器进行一些基础的绑定和服务注册。Laravel 首先会将容器实例与 app 和 Illuminate\Container\Container 进行绑定;之后,Laravel 会将基础的服务提供者注册到容器实例中,包括事件、日志、路由服务提供者;最后,Laravel 会将框架核心 class 与其相对应的别名一起注册到容器实例当中。
// namespace Illuminate\Foundation\Application
public function __construct($basePath = null)
{
if ($basePath) {
$this->setBasePath($basePath);
}
$this->registerBaseBindings();
$this->registerBaseServiceProviders();
$this->registerCoreContainerAliases();
}
protected function registerBaseBindings()
{
static::setInstance($this);
$this->instance('app', $this);
$this->instance(Container::class, $this);
/* ... ... */
}
protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
public function registerCoreContainerAliases()
{
foreach ([
'app' => [self::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class],
/* ... ...*/
'db' => [\Illuminate\Database\DatabaseManager::class, \Illuminate\Database\ConnectionResolverInterface::class],
'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class],
/* ... ... */
'request' => [\Illuminate\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class],
'router' => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],
/* ... ... */
] as $key => $aliases) {
foreach ($aliases as $alias) {
$this->alias($key, $alias);
}
}
}
// namespace Illuminate\Container\Container
public function alias($abstract, $alias)
{
if ($alias === $abstract) {
throw new LogicException("[{$abstract}] is aliased to itself.");
}
$this->aliases[$alias] = $abstract;
$this->abstractAliases[$abstract][] = $alias;
}
在完成这三步基本的注册之后,我们可以很方便的访问已经注册到容器中的对象实例。例如,可以直接通过 $app['app'] 或 $app['Illuminate\Container\Container'] 访问容器本身,还可以通过 $app['db'] 直接访问数据库连接。
注册服务提供者
在容器创建的过程中会注册基础服务提供者,其注册过程通过调用 register() 方法完成。
// namespace Illuminate\Foundation\Application
public function register($provider, $force = false)
{
if (($registered = $this->getProvider($provider)) && ! $force) {
return $registered;
}
if (is_string($provider)) {
$provider = $this->resolveProvider($provider);
}
$provider->register();
if (property_exists($provider, 'bindings')) {
foreach ($provider->bindings as $key => $value) {
$this->bind($key, $value);
}
}
if (property_exists($provider, 'singletons')) {
foreach ($provider->singletons as $key => $value) {
$this->singleton($key, $value);
}
}
$this->markAsRegistered($provider);
if ($this->isBooted()) {
$this->bootProvider($provider);
}
return $provider;
}
Laravel 首先会判断指定的服务提供者是否已经在容器中注册(通过调用 getProvider() 方法实现),如果指定的服务提供者已经在容器中注册,并且本次注册操作并非强制执行,那么直接返回已经注册好的服务提供者。
如果不满足上述条件,那么 Laravel 就会开始注册服务提供者。此时,如果传参为字符串,那么 Laravel 会默认参数为服务提供者的 class 名称并进行实例化(通过 resolveProvider() 方法实现)。之后,就会调用服务提供者定义的 register() 方法进行注册。以日志服务提供者为例,其 register() 方法的方法体如下
// namespace Illuminate\Log\LogServiceProvider
public function register()
{
$this->app->singleton('log', function ($app) {
return new LogManager($app);
});
}
register() 方法的作用就是将 Illuminate\Log\LogManager 对象以单例的模式注册到容器当中,注册完成之后,容器的 $bindings 属性中会增加一项
$app->bindings['log'] = [
'concrete' => 'Illuminate\Log\LogManager {#162}',
'shared' => true,
];
如果服务提供者自身还定义了 $bindings 属性以及 $singletons 属性,那么 Laravel 还会调用相应的 bind() 方法和 singleton() 方法完成这些服务提供者自定义的绑定的注册。
这之后 Laravel 会将服务提供者标记为已经注册的状态,随后会调用服务提供者定义的 boot() 方法启动服务提供者(前提是应用已经启动)。
在向容器中注册绑定时,有 bind() 和 singleton() 两种方法,其区别仅在于注册的绑定是否为单例模式,即 shared 属性是否为 true 。
// namespace Illuminate\Container\Container
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
public function bind($abstract, $concrete = null, $shared = false)
{
// 删除旧的绑定
$this->dropStaleInstances($abstract);
if (is_null($concrete)) {
$concrete = $abstract;
}
if (! $concrete instanceof Closure) {
if (! is_string($concrete)) {
throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
}
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
protected function getClosure($abstract, $concrete)
{
return function ($container, $parameters = []) use ($abstract, $concrete) {
if ($abstract == $concrete) {
return $container->build($concrete);
}
return $container->resolve(
$concrete, $parameters, $raiseEvents = false
);
};
}
仍然以日志服务提供者为例,日志服务提供者在注册时以单例模式进行注册,并且 $concrete 参数为闭包。在绑定开始之前,Laravel 首先会删除旧的绑定。由于此时 $concrete 为闭包,所以 Laravel 并不会进行什么操作,只是将绑定信息存入 $bindings 属性当中。
访问服务
在服务提供者注册完成之后,我们可以用上文提到的类似访问数据库连接的方式那样访问服务。仍然以日志服务为例,我们可以通过 $app['log'] 的方式访问日志服务。另外,在 Laravel 中,我们还可以使用 facade 的方式访问服务,例如,我们可以调用 Illuminate\Support\Facades\Log::info() 来记录日志。
// namespace Illuminate\Support\Facades\Log
class Log extends Facade
{
protected static function getFacadeAccessor()
{
return 'log';
}
}
// namespace Illuminate\Support\Facades\Facade
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
/* ... ... */
return $instance->$method(...$args);
}
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
if (static::$app) {
return static::$resolvedInstance[$name] = static::$app[$name];
}
}
在通过静态调用的方式进行日志记录时,首先会访问 Facade 中的魔术方法 __callStatic() ,该方法的首先进行的就是解析出 facade 对应的服务实例,然后调用该服务实例下的方法来执行相应的功能。每个 facade 中都会定义一个 getFacadeAccessor() 方法,这个方法会返回一个 tag,在日志服务中,这个 tag 就是日志服务提供者的闭包在容器的 $bindings 属性中的 key。也就是说,通过 facade 方式最终得到的是 $app['log']。
那么为什么可以通过关联数组的方式访问容器中注册的对象/服务?Illuminate\Container\Container 实现了 ArrayAccess 并且定义了 OffsetGet() 方法,而 Illuminate\Foundation\Application 继承了 Container ,$app 为 Application 实例化的对象,所以通过关联数组的方式访问容器中注册的对象时会访问 Container 的 OffsetGet() 方法。在 OffsetGet() 方法中会调用 Container 的 make() 方法,而 make() 方法中又会调用 resolve() 方法。resolve() 方法最终会解析并返回相应的对象。
// namespace Illuminate\Container
public function offsetGet($key)
{
return $this->make($key);
}
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
/* ... ... */
$this->with[] = $parameters;
if (is_null($concrete)) {
$concrete = $this->getConcrete($abstract);
}
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
/* ... ... */
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
protected function getConcrete($abstract)
{
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
}
protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}
public function build($concrete)
{
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
/* ... ... */
}
protected function getLastParameterOverride()
{
return count($this->with) ? end($this->with) : [];
}
这里需要说明,在通过 $app['log'] 的方式解析日志服务实例时,resolve() 方法中的 $concrete 解析得到的是一个闭包,导致 isBuildable() 方法返回结果为 true,所以 Laravel 会直接调用 build() 方法。而由于此时 $concrete 是一个闭包,所以在 build() 方法中会直接执行这个闭包函数,最终返回 LogManager 实例。
在基础的绑定和服务注册完成之后,容器创建成功并返回 $app 。之后 Laravel 会将内核(包括 Http 内核和 Console 内核)和异常处理注册到容器当中。然后 Laravel 开始处理请求。
// namespace bootstrap/app.php
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
// public/index.php
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Request::capture()
)->send();
$kernel->terminate($request, $response);
在开始处理请求之前,Laravel 首先会解析出 Http 内核对象 $kernel,即 App\Http\Kernel 实例化的对象。而 App\Http\Kernel 继承了 Illuminate\Foundation\Kernel,所以 $kernel 实际调用的是 Illuminate\Foundation\Kernel 中的 handle() 方法。
namespace Illuminate\Foundation\Http
use Illuminate\Contracts\Debug\ExceptionHandler
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Throwable $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new RequestHandled($request, $response)
);
return $response;
}
// 上报错误
protected function reportException(Throwable $e)
{
$this->app[ExceptionHandler::class]->report($e);
}
// 渲染错误信息
protected function renderException($request, Throwable $e)
{
return $this->app[ExceptionHandler::class]->render($request, $e);
}
handle() 方法在处理请求的过程中如果出现任何异常或错误,Laravel 都会调用容器中已经注册好的异常处理对象来上报异常并且渲染返回信息。
在容器创建成功以后,Laravel 会将 Illuminate\Contracts\Debug\ExceptionHandler 和 App\Exceptions\Handler 之间的绑定注册到容器当中,所以 Laravel 处理异常实际调用的都是 App\Exceptions\Handler 中的方法。在实际开发过程中,开发者可以根据自身需要在 App\Exceptions\Handler 中自定义 report() 和 render() 方法。
在 PHP 7 中,`Exception` 和 `Error` 是两种不同的类型,但它们同时都继承了 `Throwable` ,所以 `handler()` 方法中捕获的是 `Throwable` 对象。
在正式开始处理请求之前,Laravel 会进行一些引导启动,包括加载环境变量、配置信息等,这些引导启动在 Laravel 运行过程中起到了非常重要的作用。
// namespace Illuminate\Foundation\Http\Kernel
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
protected function sendRequestThroughRouter($request)
{
/* ... ... */
$this->bootstrap();
/* ... ... */
}
public function bootstrap()
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}
// namespace Illuminate\Foundation\Application
public function bootstrapWith(array $bootstrappers)
{
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {
$this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]);
$this->make($bootstrapper)->bootstrap($this);
$this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]);
}
}
从代码中可以看出,引导启动的过程实际就是调用各个 class 中的 bootstrap() 方法。其中:
LoadEnvironmentVariables 用来加载环境变量
LoadConfiguration 用来加载 config 目录下的配置文件
HandleExceptions 用来设置 PHP 的错误报告级别以及相应的异常和错误处理函数,另外还会设置 PHP 的程序终止执行函数
// namespace Illuminate\Foundation\Bootstrap\HandleExceptions
public function bootstrap(Application $app)
{
/* ... ... */
$this->app = $app;
error_reporting(-1);
set_error_handler([$this, 'handleError']);
set_exception_handler([$this, 'handleException']);
register_shutdown_function([$this, 'handleShutdown']);
/* ... ... */
}
public function handleError($level, $message, $file = '', $line = 0, $context = [])
{
if (error_reporting() & $level) {
/* ... ... */
throw new ErrorException($message, 0, $level, $file, $line);
}
}
public function handleException(Throwable $e)
{
/* ... ... */
$this->getExceptionHandler()->report($e);
/* ... ... */
}
public function handleShutdown()
{
if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
$this->handleException($this->fatalErrorFromPhpError($error, 0));
}
}
protected function getExceptionHandler()
{
return $this->app->make(\Illuminate\Contracts\Debug\ExceptionHandler::class);
}
从以上代码中可以看出,虽然 HandleExceptions 中定义了异常、错误、程序终止的处理函数,但无论是哪种情况,最终还是调用 App\Exceptions\Handler 中的方法来处理异常或错误。
RegisterFacades 的作用一个是注册配置文件以及第三方包中自定义的 alias 类,还有一个非常重要的作用就是为 Illuminate\Support\Facades\Facade 类设置 $app 属性。
// namespace Illuminate\Foundation\Bootstrap\RegisterFAcades
public function bootstrap(Application $app)
{
Facade::clearResolvedInstances();
Facade::setFacadeApplication($app);
AliasLoader::getInstance(array_merge(
$app->make('config')->get('app.aliases', []),
$app->make(PackageManifest::class)->aliases()
))->register();
}
&emsp 我们在通过 facade 方式反问容器中注册的服务时,Facade 在解析容器中的服务实例时用到的 static::$app 即是在这个时候设置的。
RegisterProviders 的作用是注册配置文件以及第三方包中定义的服务提供者
// namespace Illuminate\Foundation\Bootstrap\RegisterProviders
public function bootstrap(Application $app)
{
$app->registerConfiguredProviders();
}
public function registerConfiguredProviders()
{
$providers = Collection::make($this->make('config')->get('app.providers'))
->partition(function ($provider) {
return strpos($provider, 'Illuminate\\') === 0;
});
$providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);
(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
->load($providers->collapse()->toArray());
}
在实际注册的过程中,Laravel 会按照 Laravel 框架的服务提供者 > 第三方包的服务提供者 > 开发者自定义的服务提供者 的顺序进行注册
BootProviders 则是按顺序调用已经注册到容器中的服务提供者的 boot() 方法(前提是服务提供者定义的 boot() 方法)
在引导启动完成之后,Laravel 开始处理请求,首先要做的就是将全局的中间件应用于 request 。这之后 Laravel 会将请求分发到相应的路由进行处理,处理之前需要先根据 request 找到相应的路由对象 Illuminate\Routing\Route。在 Laravel 中,除了全局中间件,还有一些中间件只作用于特定的路由或路由分组,此时这些中间件就会被作用于 request 。这些工作都完成之后,路由对象开始执行代码,完成请求。
// namespace Illuminate\Foundation\Http\Kernel
protected function sendRequestThroughRouter($request)
{
/* ... ... */
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
// namespace Illuminate\Routing\Router
public function dispatch(Request $request)
{
$this->currentRequest = $request;
return $this->dispatchToRoute($request);
}
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
protected function runRoute(Request $request, Route $route)
{
/* ... ... */
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
protected function runRouteWithinStack(Route $route, Request $request)
{
/* ... ... */
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
});
}
Laravel 中的路由在注册时,action 可以是控制器方法,也可以是闭包。但无论是那种形式,都需要传参,而传参就会遇到需要依赖注入的情况。
Route 对象在执行 run() 方法时会根据 action 的类型分别进行控制器方法调用或闭包函数的调用。但两种方法最终都需要解析参数,而如果参数中用到了 class ,就需要进行依赖注入。
// namespace Illuminate\Routing\Router
public function run()
{
$this->container = $this->container ?: new Container;
try {
if ($this->isControllerAction()) {
return $this->runController();
}
return $this->runCallable();
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}
protected function runController()
{
return $this->controllerDispatcher()->dispatch(
$this, $this->getController(), $this->getControllerMethod()
);
}
protected function runCallable()
{
/* ... ... */
return $callable(...array_values($this->resolveMethodDependencies(
$this->parametersWithoutNulls(), new ReflectionFunction($callable)
)));
}
// namespace Illuminate\Routing\ControllerDispatcher
public function dispatch(Route $route, $controller, $method)
{
$parameters = $this->resolveClassMethodDependencies(
$route->parametersWithoutNulls(), $controller, $method
);
/* ... ... */
}
// namespace Illuminate\Routing\RouteDependencyResolverTrait
protected function resolveClassMethodDependencies(array $parameters, $instance, $method)
{
/* ... ... */
return $this->resolveMethodDependencies(
$parameters, new ReflectionMethod($instance, $method)
);
}
public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector)
{
/* ... ... */
foreach ($reflector->getParameters() as $key => $parameter) {
$instance = $this->transformDependency($parameter, $parameters, $skippableValue);
/* ... ... */
}
return $parameters;
}
protected function transformDependency(ReflectionParameter $parameter, $parameters, $skippableValue)
{
$className = Reflector::getParameterClassName($parameter);
if ($className && ! $this->alreadyInParameters($className, $parameters)) {
return $parameter->isDefaultValueAvailable() ? null : $this->container->make($className);
}
return $skippableValue;
}
在执行过程中,Laravel 首先通过反射取得参数列表(对于控制器方法,使用 ReflectionMethod ,对于闭包函数,则使用 ReflectionFunction )。在得到参数列表后,Laravel 仍然是利用反射,逐个判断参数类型。如果参数类型为 PHP 的内置类型,那么不需要什么特殊处理;但如果参数不是 PHP 内置类型,则需要利用反射解析出参数的具体类型。在解析出参数的具体类型之后,紧接着会判断该类型的对象是不是已经存在于参数列表中,如果不存在并且该类型也没有设置默认值,那么就需要通过容器创建出该类型的实例。
要通过容器创建指定 class 的实例,仍然需要用到 resolve() 方法。前文已经叙述过使用 resolve() 方法解析闭包函数的情况,所以这里值叙述实例化 class 的情况。
// namespace Illuminate\Container\Container
public function build($concrete)
{
/* ... ... */
try {
$reflector = new ReflectionClass($concrete);
} catch (ReflectionException $e) {
throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
}
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();
try {
$instances = $this->resolveDependencies($dependencies);
} catch (BindingResolutionException $e) {
array_pop($this->buildStack);
throw $e;
}
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}
protected function resolveDependencies(array $dependencies)
{
$results = [];
foreach ($dependencies as $dependency) {
if ($this->hasParameterOverride($dependency)) {
$results[] = $this->getParameterOverride($dependency);
continue;
}
$result = is_null(Util::getParameterClassName($dependency))
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);
if ($dependency->isVariadic()) {
$results = array_merge($results, $result);
} else {
$results[] = $result;
}
}
return $results;
}
容器在实例化 class 的时候,仍然是通过反射获取 class 基本信息。对于一些无法进行实例化的 class (例如 interface 、abstract class ),Laravel 会抛出异常;否则 Laravel 会继续获取 class 的构造函数的信息。对于不存在构造函数的 class ,意味着这些 class 在实例化的时候不需要额外的依赖,可以直接通过 new 来实例化;否则仍然是通过反射解析出构造函数的参数列表信息,然后逐个实例化这些参数列表中用到的 class 。在这些参数列表中的 class 都实例化完成之后,通过容器创建 class 的准备工作也已经完成,此时容器可以顺利创建出指定 class 的实例,然后注入到控制器方法或闭包中。
到此,关于“Laravel容器、控制反转和依赖注入实例分析”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。