PHP Generator相关的设计失误

PHP的Generator,也就是 yield/yield from 语法,使得函数调用可以“暂停”执行,并保留上下文,并在后续可以恢复执行。

但是,在PHP后续的设计中,很多地方都没有考虑到Generator:

Return Type Declarations(返回类型声明)

RFC见https://wiki.php.net/rfc/return_types。简而言之,可以给函数声明返回类型。先来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
declare(strict_types=1);

function inner(): Iterator
{
yield __FUNCTION__;
return 1;
}

function gen(): Generator
{
yield __FUNCTION__;
return yield from inner();
}

try {
$gen = gen();
foreach ($gen as $yielded) {
echo "yield: ".$yielded . PHP_EOL;
}
echo "return: " . $gen->getReturn() . PHP_EOL;
} catch (Exception $e) {
echo PHP_EOL;
echo $e->getTraceAsString();
echo PHP_EOL;
}

gen/inner函数是一个generator,yield的的类型是string,return的类型是int。但为了让PHP不报错,gen/inner函数的返回类型只能声明为Iterator或Generator——这完全破坏了返回类型声明的初衷:

对于普通函数,返回类型声明用来表示返回类型;对于generator,返回类型声明用来表示这是一个迭代器/Generator。(听着很不一致的样子)

好,没关系,无伤大雅。没法通过返回类型声明来推倒类型而已,我们通过PhpDoc来总可以了吧?

通过PhpDoc来标记返回类型

但是这个怎么写呢?对于刚刚的inner函数(yield的的类型是string,return的类型是int),最自然的写法是这样:


Java中的SPI机制

SPI 全称为 (Service Provider Interface) ,是Java 1.6之后内置的一种服务提供发现机制。SPI可以通过配置来替换服务(或者说interface)的实现;比如java.sql.Driver接口,可以很轻松的从MySQL切换到MongoDB实现。

问题的核心在于,如何根据interface查找对应的实现。

SPI的实现

Java 1.6中,开发者只需要在META-INF/services下添加文件,即可切换、修改对应interface的实现。文件名为实现的接口,文件内容为N个实现的类名,按行分隔。比如:

1
2
3
$ cat src/main/resources/META-INF/services/com.robberphex.Plugin 
com.robberphex.impl.PluginImpl1
com.robberphex.impl.PluginImpl2

使用起来,主要就是ServiceLoader类

1
2
3
4
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
for (Plugin plugin : loader) {
System.out.println(plugin.getClass().getCanonicalName());
}

要注意的是,ServiceLoader#load方法是CallerSensitive的,即load的时候用的是调用者的类加载器。

Java 9

Java 9之后,由于模块化的引入,所以SPI机制也做了扩展:除了原有的通过META-INF/services来注册服务外,还可以通过module中的provides…with语句来注册服务:


Java的类/实例初始化过程

昨天看到群里面有人分享了一道题目,我答错了,于是趁机了解了下Java的类/对象初始化过程:

程序的输出见文章最后。

程序A主要考察的是类实例初始化。简单验证了下,类实例初始化过程如下:

  • 父类实例初始化
  • 构造块/变量初始化(按照文本顺序执行)
  • 构造函数

程序B考察的则是类初始化。类初始化的过程如下:

  • 父类初始化
  • static变量初始化/static块(按照文本顺序执行)

但是我们必须做到面向接口编程,而不是面向实现编程(Program to an ‘interface’, not an ‘implementation’)。

于是就得看看Java Language Specification是如何规定的。其中类初始化过程如下:

  1. 每个类都有一个初始化锁LC,进程获取LC(如果没有获取到,就一直等待)
  2. 如果C正在被其他线程初始化,释放LC并等待C初始化完成
  3. 如果C正在被本线程初始化,即_递归初始化_,释放LC
  4. 如果C已经被初始化了,释放LC
  5. 如果C处于erroneous状态,释放LC并抛出异常NoClassDefFoundError
  6. 否则,将C标记为正在被本线程初始化,释放LC;然后,初始化那些final且为基础类型的类成员变量
  7. 初始化C的父类SC和各个接口SI_n(按照implements子句中的顺序来) ;如果SC或SIn初始化过程中抛出异常,则获取LC,将C标记为erroneous,并通知所有线程,然后释放LC,然后再抛出同样的异常。
  8. 从classloader处获取assertion是否被打开
  9. 接下来,按照文本顺序执行类变量初始化和静态代码块,或接口的字段初始化,把它们当作是一个个单独的代码块。
  10. 如果执行正常,获取LC,标记C为已初始化,并通知所有线程,然后释放LC
  11. 否则,如果抛出了异常E。若E不是Error,则以E为参数创建新的异常ExceptionInInitializerError作为E。如果因为OutOfMemoryError导致无法创建ExceptionInInitializerError,则将OutOfMemoryError作为E。
  12. 获取LC,将C标记为erroneous,通知所有等待的线程,释放LC,并抛出异常E。

JVM如何获取当前容器的资源限制

最近同事说到Java的 ParallelGCThreads 参数,我翻了下jdk8的代码,发现 ParallelGCThreads 的参数默认值如下:

  • 如果cpu核心数目少于等于8,则GC线程数量和CPU数一致
  • 如果cpu核心数大于8,则前8个核,每个核心对应一个GC线;其他核,每8个核对应5个GC线程

但是被提醒,发现即使在分配4核的容器上,GC线程数也为38。然后就想到应该和容器的资源限制有关——jvm可能无法觉察到当前容器的资源限制。

翻了下代码,发现最新版本的java是能感知容器的资源限制的,就按照jdk版本再翻了下代码:

线上的jdk(jdk8u144)

写一个sleep 1000s的程序,用于查看JVM的线程数量:

1
./jdk1.8.0_144/bin/java -XX:+UseG1GC -XX:+ParallelRefProcEnabled Main 

然后查看GC线程数目:

1
2
$ jstack $pid | grep 'Parallel GC Threads' | wc -l
38

一算就知道物理机器有56个核心(8+(56-8)*5/8=38)


StringBuffer,StringBuilder以及String

今天在网上闲逛,看见 @姚冬 的一个回答

他提到的问题也很有深度,然后思考了下,想评论来着。然而评论区太小,写不下,所以单独写在这儿。

基本上可以当作快问快答来读…

为什么java中的string不以\0结尾?

  • \0结尾在很大程度上要求程序员写规范的代码,如果写出了不规范的代码,那么很容易就内存越界了。
  • 另外,string的内部存储是char[],而为了内存安全,java数组本来就有一个length属性,这时以\0结尾就是一个多余的设计了。
  • String的内部存储也只能是char[]了,如果是其他的方式,比如通过native内部放一个c风格的数组,那么java代码中的char[]和string的转换就要很多内存拷贝操作了。
  • 而C语言设计成\0结尾,是为了减少抽象层,让C语言更加贴近硬件

(在语言设计中,)字符串的长度放哪里,放到起始指针的位置,还是起始指针的前面 ?

  • Java中,String的length也就是数组的length,JLS也只是说明了arraylength字节码,没有规定如何实现
  • 不过Hot Spot的实现是,先元数据,再长度,再具体的内容(比如char[])

如果放前面,那么字符串起始指针和内存块起始不一致怎么解决

Java不存在这个问题,我觉得。元数据和length字段都在实际数组之前呢。Java中,访问任何对象之前都要再多一次跳转,跳过元数据(和length)。

字符串拼接的时候把源串复制到目标串结尾,那么目标串剩余内存不够怎么办,重新分配要多一次赋值,频繁拼接性能有问题怎么办


vscjava.vscode-java-debug 0.18.0的新特性!

微软为VSCode开发了一个Java调试器 Debugger for Java。之前用这个很不爽,还和微软的人吐槽过VSCode在debug java的时候,只能看到HashMap等java自带数据结构的物理视图,比如一个HashMap,在 0.17.0 版本下debug时,是这样的:

HashMap里面有很多实现的细节,但是一般在debug的时候,我们比较关注的是这个HashMap里面存储了哪些东西等,而不是这种具体实现的细节。

然后0.18.0就实现了HashMap的逻辑视图,就是只查看数据,而不查看实现的视图:

在调试的时候,可以很方便的查看容器数据类型内的数据。

然后再仔细看了下Debugger for Java 的Changelog,发现还有一个比较有用的更新:

Add the source hyperlinks for the stack traces in the Debug Console output.

比如异常打印的StackTrace,在0.17.0是这样的:


Python 2/3下如何处理cjk编码的zip文件

今天项目中遇到了中文编码的zip文件,处理了蛮长时间,所以记录下,以免下次踩坑。

Python2下

Python2中读取zip文件,zipfile.ZipInfo的filename类型是str,基本上类似于python3中的bytes,即可以被decode为unicode。

所以,要处理中文,只需要将文件名按照编码decode成unicode就好。

1
2
3
4
5
6
7
8
import zipfile

fpath = '/path/to/zip.zip'
zfile = zipfile.ZipFile(fpath, 'r')
for fileinfo in zfile.filelist:
print fileinfo.filename.decode('gb18030')
# 如果要更加详细的区分bytes/str/unicode的语义
print bytes(fileinfo.filename).decode('gb18030')

Python3下

Python3中,Language encoding flag (EFS)如果是1,则按照utf8来处理文件编码,EFS如果为0,则直接按照cp437解码文件名。这是标准直接规定的。

但是,很多软件在制作zip压缩包的时候,直接使用gb18030或者其他非标准编码格式来编码文件名,所以我们还得将文件名反转为bytes,然后再使用对应的编码方式解码:

1
2
3
4
fpath = '/path/to/zip.zip'
zfile = zipfile.ZipFile(fpath, 'r')
for fileinfo in zfile.filelist:
print(fileinfo.filename.encode('cp437').decode('gb18030'))


为何一次请求会有两次HttpServlet:service调用?

今天看了下阿里出的 Arthas使用文档中有问到:

为什么只访问了http://localhost:8080/a.txt,但Arthas的trace命令打印出了两个请求树?

然后我去自己试了下,发现还真的是这个情况,trace日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
`---ts=2019-02-24 18:05:20;thread_name=http-nio-8080-exec-1;id=15;is_daemon=true;priority=5;TCCL=org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedWebappClassLoader@6601a0f8
`---[22.125552ms] javax.servlet.http.HttpServlet:service()
`---[22.048005ms] javax.servlet.http.HttpServlet:service()
`---[21.977486ms] org.springframework.web.servlet.FrameworkServlet:service()
+---[0.015829ms] javax.servlet.http.HttpServletRequest:getMethod()
+---[0.023379ms] org.springframework.http.HttpMethod:resolve()
`---[21.847595ms] org.springframework.web.servlet.HttpServletBean:service()
`---……

`---ts=2019-02-24 18:05:20;thread_name=http-nio-8080-exec-1;id=15;is_daemon=true;priority=5;TCCL=org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedWebappClassLoader@6601a0f8
`---[108.691709ms] javax.servlet.http.HttpServlet:service()
`---[108.658563ms] javax.servlet.http.HttpServlet:service()
`---[108.612929ms] org.springframework.web.servlet.FrameworkServlet:service()
+---[0.024764ms] javax.servlet.http.HttpServletRequest:getMethod()
+---[0.004569ms] org.springframework.http.HttpMethod:resolve()
`---[108.540682ms] org.springframework.web.servlet.HttpServletBean:service()
`---……

然后,过去翻了一下代码才发现,是org.apache.catalina.core.StandardHostValve#invoke方法的逻辑:

1
2
3
4
5
6
7
8
9
10
//正常的处理请求(这是第一个HttpServlet:service调用)  
context.getPipeline().getFirst().invoke(request, response);
// 如果response是失败的,那么再处理ErrorPage的逻辑
if (response.isErrorReportRequired()) {
if (t != null) {
throwable(request, response, t);
} else {
status(request, response);
}
}

在status方法中,获取ErrorPage,然后给request设置javax.servlet.error.*的属性,然后再forward到HttpServlet:service。这儿就会出现第二个HttpServlet:service请求。

那么问题来了,这个行为是规范里面有约定吗?

翻了翻 Java Servlet Specification,第10.9.2小节有说:

To allow developers to customize the appearance of content returned to a Web client when a servlet generates an error, the deployment descriptor defines a list of error page descriptions. The syntax allows the configuration of resources to be returned by the container either when a servlet or filter calls sendError on the response for specific status codes, or if the servlet generates an exception or error that propagates to the container.

If the sendError method is called on the response, the container consults the list of error page declarations for the Web application that use the status-code syntax and attempts a match. If there is a match, the container returns the resource as indicated by the location entry.

The Web application may have declared error pages using the exception-typeelement. In this case the container matches the exception type by comparing the exception thrown with the list of error-page definitions that use the exception-typeelement. A match results in the container returning the resource indicated in the location entry. The closest match in the class hierarchy wins.


PHP/Composer是如何加载一个类的

PHP/composer开发中,我们只需要require 'vendor/autoload.php',然后就可以直接使用各种类了。那么这些类是如何加载的呢?其中有没有什么可以优化的点呢?

概览

PHP/composer下,类的加载主要到如下部分(还没有包括各个部分的初始化逻辑):

1
2
3
4
5
6
7
8
PHP中zend_lookup_class_ex
|-> EG(class_table)
|-> spl_autoload_call
|-> ComposerAutoloadClassLoader::loadClass
|-> findFile
|-> class map lookup
|-> PSR-4 lookup
|-> PSR-0 lookup

PHP的类加载

首先,PHP在运行的时候,需要一个类,是通过zend_lookup_class_ex来找到这个类的相关信息的。

zend_lookup_class_ex查找类的主要逻辑如下(假设类名字放到变量lc_name中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
ZEND_API zend_class_entry *zend_lookup_class_ex(zend_string *name, const zval *key, int use_autoload) /* {{{ */
{
// 1. 类名字转化为小写
if (ZSTR_VAL(name)[0] == '') {
lc_name = zend_string_alloc(ZSTR_LEN(name) - 1, 0);
zend_str_tolower_copy(ZSTR_VAL(lc_name), ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1);
} else {
lc_name = zend_string_tolower(name);
}

// 2. 直接在class_table中查找
ce = zend_hash_find_ptr(EG(class_table), lc_name);
if (ce) {
if (!key) {
zend_string_release(lc_name);
}
return ce;
}
// 3. 如果没有autoload_func,则注册默认的__autoload
if (!EG(autoload_func)) {
zend_function *func = zend_hash_str_find_ptr(EG(function_table), ZEND_AUTOLOAD_FUNC_NAME, sizeof(ZEND_AUTOLOAD_FUNC_NAME) - 1);
if (func) {
EG(autoload_func) = func;
} else {
if (!key) {
zend_string_release(lc_name);
}
return NULL;
}

}

// 4. 加载ACLASS的过程中,又加载ACLASS,递归加载,直接找不到类
if (zend_hash_add_empty_element(EG(in_autoload), lc_name) == NULL) {
if (!key) {
zend_string_release(lc_name);
}
return NULL;
}

// 5. 调用autoload_func
ZVAL_STR_COPY(&fcall_info.function_name, EG(autoload_func)->common.function_name);
fcall_info.symbol_table = NULL;

zend_exception_save();
if ((zend_call_function(&fcall_info, &fcall_cache) == SUCCESS) && !EG(exception)) {
ce = zend_hash_find_ptr(EG(class_table), lc_name);
}
zend_exception_restore();

if (!key) {
zend_string_release(lc_name);
}
return ce;
}
  1. lc_name转化成小写(这说明PHP中类名字不区分大小写)
  2. 然后在EG(class_table)找,如果找到,直接返回(我们自己注册的类,扩展注册的类都是这样找到的)
  3. 然后查看EG(autoload_func),如果没有则将__autoload注册上(值得注意的是,如果注册了EG(autoload_func),则不会走__autoload)
  4. 通过EG(in_autoload)判断是否递归加载了(EG(in_autoload)是一个栈,记载了那些类正在被autoload加载)
  5. 然后调用EG(autoload_func),并返回类信息

SPL扩展注册


How to use mysql-connector-python 2.1.5 on Django 1.10?

MySQL Connector/Python currently not available from PyPI

When you want use mysql-connector-python as db backend in Django, the first thing you noticed is that the latest version of mysql-connector-python on PyPI is 2.0.4, but official version is 2.1.5. The official developer explained this.(But why not host mysql-connector-python on PyPI?!). so, you can bypass this problem. Add follow line into requirements.txt:

https://dev.mysql.com/get/Downloads/Connector-Python/mysql-connector-python-2.1.5.tar.gz#egg=mysql-connector-python

when you configured Connector/Python Django Backend, and run python manage.py migrate , you will meet error:

DatabaseError: Data truncated for column ‘applied’ at row 1

This is because mysql.connector.django cannot handle datetime with timezone. You need patch 93c7b38. when you fix this, you must drop all table, and re-run python manage.py migrate, you will meet another error:

File “/path/to/venv/lib/python3.5/site-packages/mysql/connector/django/operations.py”, line 223, in bulk_insert_sql
return “VALUES “ + “, “.join([items_sql] * num_values)
TypeError: can’t multiply sequence by non-int of type ‘tuple’

can’t multiply sequence by non-int of type ‘tuple’

This is because bulk_insert_sql ‘s third argument changed from int to list of placeholder rows, since Django 1.9. You need another patch, patch 8dab00b. (Of cause, we doesn’t drop support of Django 1.6)



Robert Lu

关注我的公众号