上一篇写到Eloquent ORM的基类Builder类,这次就来看一下这些方便的ORM方法是如何转换成sql语句运行的。

首先还是进入\vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php这个类中,先来看一下最常用的where()方法。

如下所示,where方法的代码很长,但前面多个if都是用来兼容各种不同调用式的。我们先抛开这些花哨的调用方式,来看一下最简单的调用方法是怎么运行的。

  1. /**
  2. * Add a basic where clause to the query.
  3. *
  4. * @param string|array|\Closure $column
  5. * @param mixed $operator
  6. * @param mixed $value
  7. * @param string $boolean
  8. * @return $this
  9. */
  10. public function where($column, $operator = null, $value = null, $boolean = 'and')
  11. {
  12. // If the column is an array, we will assume it is an array of key-value pairs
  13. // and can add them each as a where clause. We will maintain the boolean we
  14. // received when the method was called and pass it into the nested where.
  15. if (is_array($column)) {
  16. return $this->addArrayOfWheres($column, $boolean);
  17. }
  18.  
  19. // Here we will make some assumptions about the operator. If only 2 values are
  20. // passed to the method, we will assume that the operator is an equals sign
  21. // and keep going. Otherwise, we'll require the operator to be passed in.
  22. list($value, $operator) = $this->prepareValueAndOperator(
  23. $value, $operator, func_num_args() == 2
  24. );
  25.  
  26. // If the columns is actually a Closure instance, we will assume the developer
  27. // wants to begin a nested where statement which is wrapped in parenthesis.
  28. // We'll add that Closure to the query then return back out immediately.
  29. if ($column instanceof Closure) {
  30. return $this->whereNested($column, $boolean);
  31. }
  32.  
  33. // If the given operator is not found in the list of valid operators we will
  34. // assume that the developer is just short-cutting the '=' operators and
  35. // we will set the operators to '=' and set the values appropriately.
  36. if ($this->invalidOperator($operator)) {
  37. list($value, $operator) = [$operator, '='];
  38. }
  39.  
  40. // If the value is a Closure, it means the developer is performing an entire
  41. // sub-select within the query and we will need to compile the sub-select
  42. // within the where clause to get the appropriate query record results.
  43. if ($value instanceof Closure) {
  44. return $this->whereSub($column, $operator, $value, $boolean);
  45. }
  46.  
  47. // If the value is "null", we will just assume the developer wants to add a
  48. // where null clause to the query. So, we will allow a short-cut here to
  49. // that method for convenience so the developer doesn't have to check.
  50. if (is_null($value)) {
  51. return $this->whereNull($column, $boolean, $operator !== '=');
  52. }
  53.  
  54. // If the column is making a JSON reference we'll check to see if the value
  55. // is a boolean. If it is, we'll add the raw boolean string as an actual
  56. // value to the query to ensure this is properly handled by the query.
  57. if (Str::contains($column, '->') && is_bool($value)) {
  58. $value = new Expression($value ? 'true' : 'false');
  59. }
  60.  
  61. // Now that we are working with just a simple query we can put the elements
  62. // in our array and add the query binding to our array of bindings that
  63. // will be bound to each SQL statements when it is finally executed.
  64. $type = 'Basic';
  65.  
  66. $this->wheres[] = compact(
  67. 'type', 'column', 'operator', 'value', 'boolean'
  68. );
  69.  
  70. if (! $value instanceof Expression) {
  71. $this->addBinding($value, 'where');
  72. }
  73.  
  74. return $this;
  75. }

先从这个方法的参数开始,它一共有4个形参,分别代表$column字段、$operator操作符、$value值、$boolean = 'and'。

从字面意思我们可以猜测到,最原始的where方法,一开始是打算像$model->where('age', '>', 18)->get()这样来进行基本查询操作的。

那么让我们先抛开前面那些if代码块,直接跳到方法底部builder类通过compact函数,将基础参数添加到$this->wheres数组后,在判断$value不是一个表达式后,跳转到了addBinding方法中。

  1. public function addBinding($value, $type = 'where')
  2. {
  3.  
  4. if (! array_key_exists($type, $this->bindings)) {
  5. throw new InvalidArgumentException("Invalid binding type: {$type}.");
  6. }
  7.  
  8. if (is_array($value)) {
  9. $this->bindings[$type] = array_values(array_merge($this->bindings[$type], $value));
  10. } else {
  11. $this->bindings[$type][] = $value;
  12. }
  13.  
  14. return $this;
  15. }

接下来看addBinding方法做了什么,首先一次array_key_exists校验确定传入条件正确。然后判断传入的value是否为数组,若非数组,则直接将这个值传入$this->bindings数组的对应操作中。打印出来如下所示。

随后便直接返回了$this对象,一个最简单的where方法就执行完毕了。

那么,按正常操作,接下来就改执行get()方法了。

  1. public function get($columns = ['*'])
  2. {
  3. $original = $this->columns;
  4.  
  5. if (is_null($original)) {
  6. $this->columns = $columns;
  7. }
  8.  
  9. $results = $this->processor->processSelect($this, $this->runSelect());
  10.  
  11. $this->columns = $original;
  12.  
  13. return collect($results);
  14. }

这个方法首先获取了要查询的字段,若为空则使用传入方法的$columns参数。然后通过$this->runSelect()方法进行查询,通过processor将返回值包装返回。

让我们来看一下runSelect()方法,这里的$this->connection其实是获取到pdo的链接对象,select()方法的三个参数分别为sql语句,pdo为了防注入将语句与值给分开了,所以第二个参数为值,第三个参数则是为了通过参数获取只读或读写模式的pdo实例。

getBindings()直接从对象中获取数据,并通过laravel 的 Arr对象进行包装。

而toSql()方法想要获得sql语句却没有那么简单,它需要调用多个方法来对sql进行拼接。

  1. protected function runSelect()
  2. {
  3. return $this->connection->select(
  4. $this->toSql(), $this->getBindings(), ! $this->useWritePdo
  5. );
  6. }
  7.  
  8. public function getBindings()
  9. {
  10. return Arr::flatten($this->bindings);
  11. }
  12.  
  13. public function toSql()
  14. {
  15. return $this->grammar->compileSelect($this);
  16. }

那么现在来看一下sql语句是如何获取到的吧。compileSelect方法位于\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\Grammar.php对象中,它会通过Builder对象中的属性数据,来拼接一条sql返回出去。

  1. public function compileSelect(Builder $query)
  2. {
  3. // If the query does not have any columns set, we'll set the columns to the
  4. // * character to just get all of the columns from the database. Then we
  5. // can build the query and concatenate all the pieces together as one.
  6. $original = $query->columns;
  7.  
  8. if (is_null($query->columns)) {
  9. $query->columns = ['*'];
  10. }
  11.  
  12. // To compile the query, we'll spin through each component of the query and
  13. // see if that component exists. If it does we'll just call the compiler
  14. // function for the component which is responsible for making the SQL.
  15. $sql = trim($this->concatenate(
  16. $this->compileComponents($query))
  17. );
  18.  
  19. $query->columns = $original;
  20.  
  21. return $sql;
  22. }

这个方法一开始获取了语句要查询的字段。并做了空值判断,若为空则查询 * 。

接下来我们看一下$this->compileComponents($query)这一句代码,它的作用是返回基本的sql语句段,返回值如下所示。

然后通过$this->concatenate()方法将其拼接成一条完整的sql语句。为了搞清楚sql语句是怎么来的,我们又得深入compileComponents方法了。

这个方法位于\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\Grammar.php对象内部。先来看一下它的代码。

  1. protected function compileComponents(Builder $query)
  2. {
  3. $sql = [];
  4.  
  5. foreach ($this->selectComponents as $component) {
  6. // To compile the query, we'll spin through each component of the query and
  7. // see if that component exists. If it does we'll just call the compiler
  8. // function for the component which is responsible for making the SQL.
  9. if (! is_null($query->$component)) {
  10. $method = 'compile'.ucfirst($component);
  11. //var_dump($component,$method,$query->$component,'-------'); //将这些条件打印出来看一下
  12. $sql[$component] = $this->$method($query, $query->$component);
  13. }
  14. }
  15. //dd('over');
  16. return $sql;
  17. }

这个方法内部,将selectComponents属性,也就是查询语句模板,进行了遍历,并判断出了,在$query对象中所存在的那一部分。通过这些语句,来构建sql语句片段。这个模板如下所示。

  1. protected $selectComponents = [
  2. 'aggregate',
  3. 'columns',
  4. 'from',
  5. 'joins',
  6. 'wheres',
  7. 'groups',
  8. 'havings',
  9. 'orders',
  10. 'limit',
  11. 'offset',
  12. 'unions',
  13. 'lock',
  14. ];

而$query对象中所存在的部分,将它们打印后,结果如下所示。通过我上面代码段中被注释的部分,将其打印了出来,我在下图中对三个属性做了注释。

总结来讲,这个方法会根据builder对象中所存储的属性,运行模板方法,将其构建成sql字符串部件。而builder对象中的属性则是我们自己通过DB或Model方法添加进去的。

那么我们刚刚那句简单的sql查询则是运行了compileColumns、compileFrom、compileWheres。这三个方法。

  1. protected function compileColumns(Builder $query, $columns)
  2. {
  3. // If the query is actually performing an aggregating select, we will let that
  4. // compiler handle the building of the select clauses, as it will need some
  5. // more syntax that is best handled by that function to keep things neat.
  6. if (! is_null($query->aggregate)) {
  7. return;
  8. }
  9.  
  10. $select = $query->distinct ? 'select distinct ' : 'select ';
  11.  
  12. return $select.$this->columnize($columns);
  13. }
  14.  
  15. public function columnize(array $columns)
  16. {
  17. return implode(', ', array_map([$this, 'wrap'], $columns));
  18. }

先来看compileColumns,这个方法看上去很简单,判断aggregate不为空后,根据distinct 属性来得出sql语句头,然后将这个字符串与$this->columnize()方法的返回值进行拼接。就得出了上面'select *'这句字符串。而关键在于columnize方法中的array_map的[$this, 'wrap']。

array_map这个函数会传入两个参数,第一个参数为函数名,第二个参数为数组。将第二个数组参数中的每个值当成参数,传入第一个参数所代表的函数中循环执行。

那么现在我们要找到wrap这个方法了。

  1. public function wrap($value, $prefixAlias = false)
  2. {
  3. if ($this->isExpression($value)) {
  4. return $this->getValue($value);
  5. }
  6.  
  7. // If the value being wrapped has a column alias we will need to separate out
  8. // the pieces so we can wrap each of the segments of the expression on it
  9. // own, and then joins them both back together with the "as" connector.
  10. if (strpos(strtolower($value), ' as ') !== false) {
  11. return $this->wrapAliasedValue($value, $prefixAlias);
  12. }
  13.  
  14. return $this->wrapSegments(explode('.', $value));
  15. }

这个方法,首先判断了传入参数不是一个表达式,而是一个确定的值。然后strpos(strtolower($value), ' as ') !== false这一句将$value转为小写,并判断了sql语句中没有as字段。然后便返回了$this->wrapSegments的值。

  1. protected function wrapSegments($segments)
  2. {
  3. return collect($segments)->map(function ($segment, $key) use ($segments) {
  4. return $key == 0 && count($segments) > 1
  5. ? $this->wrapTable($segment)
  6. : $this->wrapValue($segment);
  7. })->implode('.');
  8. }

到这里,我们会发现这个方法,只是传入了一个闭包函数,就给返回了,laravel框架实在是难以跟踪。

事实上collect()方法代表了\vendor\laravel\framework\src\Illuminate\Support\Collection.php对象。

可以看到在collection类的构造方法中,我们将参数存入了它的属性,而在map方法中,通过array_keys对这些属性做了处理过后,又通过array_map对其进了加工。看下刚刚wrapSegments中的闭包函数是怎么写的,他们调用了wrapTable()和wrapValue这两个方法。根据传入参数的不同,来分别调用。

  1. public function __construct($items = [])
  2. {
  3. $this->items = $this->getArrayableItems($items);
  4. }
  5.  
  6. public function map(callable $callback)
  7. {
  8. $keys = array_keys($this->items);
  9.  
  10. $items = array_map($callback, $this->items, $keys);
  11.  
  12. return new static(array_combine($keys, $items));
  13. }
  1. protected function wrapValue($value)
  2. {
  3. if ($value !== '*') {
  4. return '"'.str_replace('"', '""', $value).'"';
  5. }
  6.  
  7. return $value;
  8. }

如果参数为*则直接返回了拼接星号的字符串,反之则直接返回了$value数组。然后视线调回collection对象的map方法,返回值在通过array_combine函数加工后,又通过collection本类包装成了对象返回。到这里函数调用就到顶了,依次返回值,返回到Grammars对象的compileColumns方法中,与'select'字符串进行拼接后再次返回。这部分sql语句片段就构建完成了。

那么接下来就剩compileFrom、compileWheres两个方法了。

  1. protected function compileFrom(Builder $query, $table)
  2. {
  3. return 'from '.$this->wrapTable($table);
  4. }
  5.  
  6. public function wrapTable($table)
  7. {
  8. if (! $this->isExpression($table)) {
  9. return $this->wrap($this->tablePrefix.$table, true);
  10. }
  11.  
  12. return $this->getValue($table);
  13. }

from语句的构建比较简单,直接from接表名就好。但是wrapTable方法中的代码我们发现有点眼熟,没错,它又调用了wrap方法,还记得我们刚刚构建select时看到的吗?这个方法只是对传入的参数做了解析,并包装成集合返回回来。其实不止select和from其他的语句段构建都要通过wrap方法来进行参数解析。刚刚已经解析过wrap方法,这里我就不多说了。最后,这个方法也是返回了'from'部分的sql语句片段。

接下来到compileWheres方法了。

  1. protected function compileWheres(Builder $query)
  2. {
  3. // Each type of where clauses has its own compiler function which is responsible
  4. // for actually creating the where clauses SQL. This helps keep the code nice
  5. // and maintainable since each clause has a very small method that it uses.
  6. if (is_null($query->wheres)) {
  7. return '';
  8. }
  9.  
  10. // If we actually have some where clauses, we will strip off the first boolean
  11. // operator, which is added by the query builders for convenience so we can
  12. // avoid checking for the first clauses in each of the compilers methods.
  13. if (count($sql = $this->compileWheresToArray($query)) > 0) {
  14. return $this->concatenateWhereClauses($query, $sql);
  15. }
  16.  
  17. return '';
  18. }
  19.  
  20. protected function compileWheresToArray($query)
  21. {
  22. return collect($query->wheres)->map(function ($where) use ($query) {
  23. return $where['boolean'].' '.$this->{"where{$where['type']}"}($query, $where);
  24. })->all();
  25. }
  26.  
  27. protected function concatenateWhereClauses($query, $sql)
  28. {
  29. $conjunction = $query instanceof JoinClause ? 'on' : 'where';
  30.  
  31. return $conjunction.' '.$this->removeLeadingBoolean(implode(' ', $sql));
  32. }
  33.  
  34. protected function removeLeadingBoolean($value)
  35. {
  36. return preg_replace('/and |or /i', '', $value, 1);
  37. }

那么,来看一下。首先compileWheres方法判断where条件是否为空,然后compileWheresToArray方法来判断where参数是否大于0。这个方法用了collect对象的map方法,我们之前已经看过了。重要的是这个闭包函数,来看一下这个闭包函数干了什么。它通过$hwere['type']这个属性中存储的字段作为方法名调用了whereBasic方法,如下所示

  1. protected function whereBasic(Builder $query, $where)
  2. {
  3. $value = $this->parameter($where['value']);
  4.  
  5. return $this->wrap($where['column']).' '.$where['operator'].' '.$value;
  6. }
  1. public function parameter($value)
  2. {
  3. return $this->isExpression($value) ? $this->getValue($value) : '?';
  4. }

通过parameter方法获取到参数后,依然是通过wrap包装参数。concatenateWhereClauses方法根据之前返回的参数,决定拼接'where'字符串,然后通过removeLeadingBoolean方法决定‘and‘等条件的拼接。

到这里,基础sql语句片段就已经全部构建出来了。

视线跳回compileSelect方法的concatenate方法。

  1. protected function concatenate($segments)
  2. {
  3. return implode(' ', array_filter($segments, function ($value) {
  4. return (string) $value !== '';
  5. }));
  6. }

通过array_filter与implode函数将sql语句片段合并为了一条完整sql语句。

sql语句有了,我们视线又要跳回Builder对象的runSelect方法了。这个里面的$this->connection->select()方法对sql进行了调用,返回的便是查询结果了。connection则是Illuminate\Database\MySqlConnection对象。

  1. protected function runSelect()
  2. {
  3. return $this->connection->select(
  4. $this->toSql(), $this->getBindings(), ! $this->useWritePdo
  5. );
  6. }

而select方法则是在它的父类\vendor\laravel\framework\src\Illuminate\Database\Connection.php中。

  1. public function select($query, $bindings = [], $useReadPdo = true)
  2. {
  3. return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
  4. if ($this->pretending()) {
  5. return [];
  6. }
  7.  
  8. // For select statements, we'll simply execute the query and return an array
  9. // of the database result set. Each element in the array will be a single
  10. // row from the database table, and will either be an array or objects.
  11. $statement = $this->prepared($this->getPdoForSelect($useReadPdo)
  12. ->prepare($query));
  13.  
  14. $this->bindValues($statement, $this->prepareBindings($bindings));
  15.  
  16. $statement->execute();
  17.  
  18. return $statement->fetchAll();
  19. });
  20. }
  21.  
  22. protected function run($query, $bindings, Closure $callback)
  23. {
  24. $this->reconnectIfMissingConnection();
  25.  
  26. $start = microtime(true);
  27.  
  28. // Here we will run this query. If an exception occurs we'll determine if it was
  29. // caused by a connection that has been lost. If that is the cause, we'll try
  30. // to re-establish connection and re-run the query with a fresh connection.
  31. try {
  32. $result = $this->runQueryCallback($query, $bindings, $callback);
  33. } catch (QueryException $e) {
  34. $result = $this->handleQueryException(
  35. $e, $query, $bindings, $callback
  36. );
  37. }
  38.  
  39. // Once we have run the query we will calculate the time that it took to run and
  40. // then log the query, bindings, and execution time so we will report them on
  41. // the event that the developer needs them. We'll log time in milliseconds.
  42. $this->logQuery(
  43. $query, $bindings, $this->getElapsedTime($start)
  44. );
  45.  
  46. return $result;
  47. }
  48.  
  49. protected function runQueryCallback($query, $bindings, Closure $callback)
  50. {
  51. // To execute the statement, we'll simply call the callback, which will actually
  52. // run the SQL against the PDO connection. Then we can calculate the time it
  53. // took to execute and log the query SQL, bindings and time in our memory.
  54. try {
  55. $result = $callback($query, $bindings);
  56. }
  57.  
  58. // If an exception occurs when attempting to run a query, we'll format the error
  59. // message to include the bindings with SQL, which will make this exception a
  60. // lot more helpful to the developer instead of just the database's errors.
  61. catch (Exception $e) {
  62. throw new QueryException(
  63. $query, $this->prepareBindings($bindings), $e
  64. );
  65. }
  66.  
  67. return $result;
  68. }

这三个方法,看起来很长一段,但是其中的代码是很简单的。我们一个一个来分析,select方法,只做了一件事,调用run方法,把sql语句,bindings参数,以及一个闭包函数传入了其中。

而run方法,则是获取了pdo链接,记录了开始查询的毫秒时间,通过runQueryCallback运行了查询闭包函数,并记录sql日志,最后返回了查询结果。

runQueryCallback方法只是简单的调用了闭包函数。现在转回来看闭包函数做了什么。

  1. $statement = $this->prepared($this->getPdoForSelect($useReadPdo)
  2. ->prepare($query));
  3.  
  4. $this->bindValues($statement, $this->prepareBindings($bindings));

关键代码就是这两句了。$this->getPdoForSelect($useReadPdo)方法通过之前设置的读写方式获取pdo实例。这里做了这么多判断,最终获取到的是provider初始化时存入的实例

  1. protected function getPdoForSelect($useReadPdo = true)
  2. {
  3. return $useReadPdo ? $this->getReadPdo() : $this->getPdo();
  4. }
  5.  
  6. public function getReadPdo()
  7. {
  8. if ($this->transactions > 0) {
  9. return $this->getPdo();
  10. }
  11.  
  12. if ($this->getConfig('sticky') && $this->recordsModified) {
  13. return $this->getPdo();
  14. }
  15.  
  16. if ($this->readPdo instanceof Closure) {
  17. return $this->readPdo = call_user_func($this->readPdo);
  18. }
  19.  
  20. return $this->readPdo ?: $this->getPdo();
  21. }

获取到pdo对象后,剩下的都是pdo的原生方法了。fetchAll方法返回sql查询结果集。

然后一直返回到get()方法。

  1. public function get($columns = ['*'])
  2. {
  3. $original = $this->columns;
  4.  
  5. if (is_null($original)) {
  6. $this->columns = $columns;
  7. }
  8.  
  9. $results = $this->processor->processSelect($this, $this->runSelect());
  10.  
  11. $this->columns = $original;
  12.  
  13. return collect($results);
  14. }

到这里,通过collect集合进行包装之后,便返回到我们model对象的操作方式了。

laravel5.5源码笔记(八、Eloquent ORM)的更多相关文章

  1. Tomcat8源码笔记(八)明白Tomcat怎么部署webapps下项目

    以前没想过这么个问题:Tomcat怎么处理webapps下项目,并且我访问浏览器ip: port/项目名/请求路径,以SSM为例,Tomcat怎么就能将请求找到项目呢,项目还是个文件夹类型的? Tom ...

  2. laravel5.5源码笔记(七、数据库初始化)

    laravel中的数据库也是以服务提供者进行初始化的名为DatabaseServiceProvider,在config文件的providers数组中有写.路径为vendor\laravel\frame ...

  3. laravel5.5源码笔记(五、Pipeline管道模式)

    Pipeline管道模式,也有人叫它装饰模式.应该说管道是装饰模式的一个变种,虽然思想都是一样的,但这个是闭包的版本,实现方式与传统装饰模式也不太一样.在laravel的源码中算是一个比较核心的设计模 ...

  4. laravel5.5源码笔记(三、门面类facade)

    上次说了provider,那么这次来说说facade 首先是启动的源头,从laravel的kernel类中的$bootstrappers 数组,我们可以看到它的一些系统引导方法,其中的Register ...

  5. laravel5.5源码笔记(六、中间件)

    laravel中的中间件作为一个请求与响应的过滤器,主要分为两个功能. 1.在请求到达控制器层之前进行拦截与过滤,只有通过验证的请求才能到达controller层 2.或者是在controller中运 ...

  6. laravel5.5源码笔记(四、路由)

    今天这篇博文来探索一下laravel的路由.在第一篇讲laravel入口文件的博文里,我们就提到过laravel的路由是在application对象的初始化阶段,通过provider来加载的.这个路由 ...

  7. laravel5.5源码笔记(二、服务提供者provider)

    laravel里所谓的provider服务提供者,其实是对某一类功能进行整合,与做一些使用前的初始化引导工作.laravel里的服务提供者也分为,系统核心服务提供者.与一般系统服务提供者.例如上一篇博 ...

  8. laravel5.5源码笔记(一、入口应用的初始化)

    laravel的项目入口文件index.php如下 define('LARAVEL_START', microtime(true)); require __DIR__.'/../vendor/auto ...

  9. Tomcat8源码笔记(三)Catalina加载过程

    之前介绍过 Catalina加载过程是Bootstrap的load调用的  Tomcat8源码笔记(二)Bootstrap启动 按照Catalina的load过程,大致如下: 接下来一步步分析加载过程 ...

随机推荐

  1. 仿饿了吗点餐界面两个ListView联动效果

    这篇文章主要介绍了仿饿了点餐界面2个ListView联动效果的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下 如图是效果图: 是仿饿了的点餐界面 1.点击左侧的ListView,通过在在适 ...

  2. EJB JBOSS的安装

    下载地址:http://www.jboss.org/jbossas/downloads 下载JBoss 4.2.3-->解压 启动:bin-->run.bat 管理后台:www.local ...

  3. Android:Gradle sync failed: Another 'refresh project' task is currently running for the project

    android studio 克隆项目后,重新导入后显示Gradle sync failed: Another 'refresh project' task is currently running ...

  4. 如何使用CSS进行网页布局(HTML/CSS)

    什么叫做布局? 又称为版式布局,是网页UI设计师将有限的视觉元素进行有机的排列组合. 题目:假设高度已知,请写出三栏布局,其中左栏和右栏宽度各为300px,中间自适应 1.浮动布局 <!DOCT ...

  5. 结对编程——四则运算器(UI第十组)

    博客目录: 一.问题描述                   二.设计思路                   三.UI开发过程                       四.对接过程       ...

  6. Java简单方法批量修改Windows文件夹下的文件名(简单IO使用)

    package test.tttt; import java.io.File; import java.util.ArrayList; import java.util.List; public cl ...

  7. How To Change Log Rate Limiting In Linux

    By default in Linux there are a few different mechanisms in place that may rate limit logging. These ...

  8. [UI] 精美UI界面欣赏[7]

    精美UI界面欣赏[7] 视频地址: http://v.youku.com/v_show/id_XOTM0MDUzNTg0.html UI介绍地址: http://www.zhihu.com/quest ...

  9. 检查windows系统支持的密码套件

    Windows 10客户端及Windows server 2016 服务器可以使用powershell 命令获得系统支持的密码套件列表,禁用启用相应的密码套件. #命令链接:https://techn ...

  10. 审计系统---堡垒机python下ssh的使用

    堡垒机python下ssh的使用 [堡垒机更多参考]http://www.cnblogs.com/alex3714/articles/5286889.html [paramiko的Demo实例]htt ...