什么是 xhprof?

xhprof 是一个轻量级 PHP 性能分析工具。

它报告函数级别的请求次数和各种指标,包括阻塞时间,CPU 时间和内存使用情况。

注意:xhprof 的使用开销很大,所以只能在本地开发调试的时候使用。

安装

我们可以通过 pecl 来安装 xhprof

1
2
# 目前最新版本是 2.3.9
pecl install xhprof-2.3.9

安装完之后,运行一下 php -m 查看是否已经启用:

1
php -m

最后,修改一下 php.ini 配置文件,添加以下配置:

1
xhprof.output_dir = /tmp/xhprof

注意:这里指定的文件夹必须有写的权限才行。

当然,我们也可以通过源码编译安装,源码在 https://github.com/longxinH/xhprof

使用

主要有两个步骤:

  1. 使用 xhprof_enable 来开启 xhprof 性能监控
1
2
3
xhprof_enable(XHPROF_FLAGS_NO_BUILTINS +
XHPROF_FLAGS_CPU +
XHPROF_FLAGS_MEMORY);
  1. 注册一个 shutdown 处理器

它的作用是在 php 请求处理完毕的时候将性能指标数据写入到文件中,如果没有这个,则在 xhprof.output_dir 中将不会有任何输出。

1
2
3
4
5
6
7
8
9
register_shutdown_function(function(){
$data = xhprof_disable(); //返回运行数据
// 需要在 https://github.com/longxinH/xhprof 下载源码,下面的 `.../xhprof_lib` 就是下载源码中的 `xhprof_lib` 目录
// 这里需要替换为自己本地的路径
include '/Users/ruby/Code/xhprof-2.3.9/xhprof_lib/utils/xhprof_lib.php';
include '/Users/ruby/Code/xhprof-2.3.9/xhprof_lib/utils/xhprof_runs.php';
$objXhprofRun = new XHProfRuns_Default();
$objXhprofRun->save_run($data, "test"); //test 表示文件后缀
});

通过 HTML 页面展示性能分析结果

  1. 我们在本地的 nginx 中添加一个 xhprof web 服务的配置:
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
server {
listen 8088;
server_name xhprof.local;
root /Users/ruby/Code/xhprof-2.3.9;

add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";

index index.php;

charset utf-8;

location / {
try_files $uri $uri/ /index.php?$query_string;
}

location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }

error_page 404 /index.php;

location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}

location ~ /\.(?!well-known).* {
deny all;
}
}
  1. 重启本地的 php-fpm

  2. 在我们的 /etc/hosts 中加上 nginx 中配置的域名即可

  3. 打开浏览器,访问 http://xhprof.local:8088/xhprof_html/

xhprof
  1. 点击其中一个进去,可以看到详情
xhprof

在这里,我们还能点击每一列的表头,让它按这一列来排序。上图我就按 Calls 逆序排序了。

以图的形式来展示

上面我们通过表格的方式来看到了函数调用的次数、时间等,但表格其实不够直观。

我们也看到上面图中的正中间有一个 View Full Callgraph 的超链接,我们可以通过这个超链接来查看具体的函数调用链, 这样我们可以更加直观的知道调用入口在哪里,以及整个调用链条大概长什么样子的。

xhprof

图太大了,这里随便看看吧

注意:要使用这个功能,我们需要安一个插件 graphviz。mac 下可以通过 brew install graphviz,其他的自行搜索。

最近在项目中发现有个接口需要耗时几分钟才能完成,排查发现跟以往的慢请求不大一样,这次的慢请求中,并没有慢查询(不管是 MySQL、MongoDB 还是 Redis、HTTP),经过排查发现其中有一个函数处理时间非常长,整个请求的 99% 的时间都花在了这个函数上:

1
2
3
4
5
6
// 函数内主要是一个 for 循环
foreach ($rules as $rule) {
if (empty($this->hasAllCustomerUserIds[$rule->user_id])) {
$this->matchRule($rule, $results);
}
}

上面这个循环有 600+ 次,但是循环内的 matchRule11w+ 个循环,也就是说,总循环次数达到了 6000w+ 次。 这样一来,就算我们没有慢查询,某些性能不高的点累积起来也会导致整个请求耗时非常漫长,以至最终耗时达到了 7 分钟。

根本原因

在这 6000w+ 的循环里面,完全没有太复杂的操作,更没有什么查询之类的操作,但是由于内层循环都是操作了 ORM 模型,有比较多的获取 ORM 模型属性的操作。 而 Laravel 的 ORM 模型中获取属性是通过魔术方法 __get() 实现的,而这个 __get() 中有一个时间复杂度比较高的操作 getAttribute(),所以这里会有一定的性能问题:

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
/**
* Dynamically retrieve attributes on the model.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->getAttribute($key);
}

/**
* Get an attribute from the model.
*
* @param string $key
* @return mixed
*/
public function getAttribute($key)
{
if (! $key) {
return;
}

// If the attribute exists in the attribute array or has a "get" mutator we will
// get the attribute's value. Otherwise, we will proceed as if the developers
// are asking for a relationship's value. This covers both types of values.
if (array_key_exists($key, $this->attributes) ||
$this->hasGetMutator($key)) {
return $this->getAttributeValue($key);
}

// Here we will determine if the model base class itself contains this given key
// since we don't want to treat any of those methods as relationships because
// they are all intended as helper methods and none of these are relations.
if (method_exists(self::class, $key)) {
return;
}

return $this->getRelationValue($key);
}

这里只贴 __get()getAttribute() 方法,但是我们也能看到了,虽然我们只是做了一个简单的获取模型属性的操作,但底层涉及了很多的方法调用。

在这种情况下,虽然在代码中已经做了一些查询上的优化,但是这个计算规模下,对 Laravel ORM 模型的操作带来的性能问题会非常显著。

因为模型中使用 $model->attribute 这种方式来获取它的属性的时候,时间复杂度会很高(相比于普通的对象属性),下面是一个性能测试:

100w 次模型访问属性操作

1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo extends \Illuminate\Database\Eloquent\Model {
}

$start = microtime(true);

$model = new Foo;
$model->a = 1;
for ($i = 0; $i < 1000000; $i++) {
$model->a;
}

// 0.7896(=789.6ms)
dump(bcsub(microtime(true), $start, 4));

最终耗时 789.6ms

100w 次普通对象获取属性操作

1
2
3
4
5
6
7
8
9
10
11
12
13
class Bar {
}
$bar = new Bar;
$bar->a = 1;

$start = microtime(true);

for ($i = 0; $i < 1000000; $i++) {
$bar->a;
}

// 0.0140(=14ms)
dump(bcsub(microtime(true), $start, 4));

最终耗时 14ms

也就是说,两者在 100w 的计算规模下,性能差距相差了 56.4 倍。

56.4 倍在数据规模小的时候,我们通常无法感知,比如 10ms,再乘以 56.4 也就是 564,这也还是一个可以接受的时间。

但是在数据规模较大的时候,原本只需要 1s 的操作,可能就得需要 56s 了,这就是非常明显的。

ORM 模型的属性访问做了什么?

首选我们需要知道的是,我们定义的模型中,并没有定义显式地定义任何 public 属性,对于这种情况,我们访问它的属性的时候,php 会去调用对象的 __get 方法。那就顺着模型的 __get 方法看看它做了什么:

orm_optimize

我们可以看到,我们只是做了一个简单的属性访问,但是底层却调用了 8+ 方法。

方法调用在次数少的时候我们无法感知,但是我们可以看看 100w 次方法调用需要多久:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test {
public function t()
{}
}

$start = microtime(true);

$test = new Test();
for ($i = 0; $i < 1000000; $i++) {
$test->t();
}

// 0.0216(=21.6ms)
dump(bcsub(microtime(true), $start, 4));

上面的代码中,我们的 t 方法里面什么都没有做,但是调用了 100w 次之依然需要 21ms

也就是说,在模型访问属性所产生的 8+ 方法调用中,至少需要花费 160ms,这还是一个保守的数字,因为上面产生的方法调用里面还有一些方法调用没有画出来。

如何优化?

知道了原因之后,我们优化起来就简单了,那就是尽量把 ORM 模型访问属性所产生的额外开销去掉,因为在这里讨论的问题中,并不需要模型帮我们做额外的处理(比如 cast、日期转换)。

因此,最简单的实现方法就是将 ORM 模型转换为普通的实体类型。

这一种优化方法下,也还有两种实现方式,先说第一种,直接创建一个 stdClass 对象,然后将 ORM 模型的属性依次赋值到这个 stdClass 对象中,如下:

在我们定义的模型中添加一个方法 toStdClass:

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
class Foo extends \Illuminate\Database\Eloquent\Model {
// 转换为 stdClass
public function toStdClass(): stdClass
{
$result = new stdClass();

foreach ($this->attributes as $key => $attribute) {
$result->{$key} = $attribute;
}

return $result;
}
}

$start = microtime(true);

$model = new Foo;
$model->a = 2;

$stdClass = $model->toStdClass();

for ($i = 0; $i < 1000000; $i++) {
$stdClass->a;
}

// 0.0140(=14ms)
dump(bcsub(microtime(true), $start, 4));

在这种实现方式中,由于我们在循环里面操作的是一个 stdClass,所以也就没有了 ORM 模型访问属性时候的方法调用开销,最终时间是 14ms,也就是跟我们访问普通对象属性的开销一样了,比原来快了 56 倍。

但是,需要注意的是,在这种实现中,由于我们拿到的是一个 stdClass,也就丧失了 ORM 模型本身带来的一些便利。不过好在这种便利在我们只是做简单的属性访问的时候是不需要的,除非我们需要在拿到模型之后还需要做 CRUD 操作。就算如此,我们也依然可以再改造一下我们的实现方式,另外定义一个实体类,使用这个实体类来代替上面的 stdClass,然后再在这个实体类中添加一个方法,让其拥有转换回 ORM 模型的能力(第 2 种实现方式):

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
class Foo extends \Illuminate\Database\Eloquent\Model
{
public function toStdClass(): stdClass
{
$result = new stdClass();

foreach ($this->attributes as $key => $attribute) {
$result->{$key} = $attribute;
}

return $result;
}
}

class FooEntity
{
// 由 ORM 模型创建一个 Entity
public static function make(\Illuminate\Database\Eloquent\Model $model): self
{
$result = new self();

foreach ($model->getAttributes() as $key => $attribute) {
$result->{$key} = $attribute;
}

return $result;
}

// 由 Entity 转换回 ORM 模型
public function toModel(): Foo
{
$result = new Foo();

foreach (get_object_vars($this) as $key => $attribute) {
$result->setAttribute($key, $attribute);
}

return $result;
}
}

$model = new Foo;
$model->a = 2;

// model -> entity
$entity = FooEntity::make($model);
// entity -> model
$model = $entity->toModel();

优化效果

这里不贴具体代码了。

在不做其他优化的情况下,只是将循环中的 ORM 模型修改为实体之后,原本 7 分钟的函数,最终只需要 44s 了。

当然,代码还有不少其他可以优化的地方,只是那些是比较常规的可以优化的,这里不再赘述。截止发文这天,这个优化已经上线了,将其他的可以优化的地方优化之后,原本 7 分钟的请求,最终耗时 20s 左右。

存在问题

  1. 上面的两种实现方式都没有显式指定类的属性,不好维护。(可以显式定义实体类,并显式定义 ORM 中存在的属性)
  2. 转换为 entity 之后无法拥有 ORM 模型的能力,需要注意。

由于第 2 点,所以这种优化不适用于那些需要更新的场景,当然我们也可以将实体转换回 ORM 模型,然后再做 CURD 操作。

总结

Laravel 的 ORM 模型给我们带来了非常大的便利,但是它通过魔术方法 __get() 来获取模型属性的方式在大数据量操作下会有一些性能问题, 如果我们不需要在这过程做 CURD 操作,我们可以将 ORM 转换为简单的实体对象。

这样就可以大大减少在 ORM 模型中访问属性的开销。

观察者模式

在说事件机制之前,我们先聊一下观察者模式,因为 Spring 的事件机制本质上是观察者模式的一种实现。

我们都知道,有一种设计模式叫观察者模式,它用于建立对象之间一对多的依赖关系,当一个对象的状态发生变化时, 所有依赖它的对象都会得到通知并自动更新。这种模式常用于需要实现对象之间松耦合的场景, 其中一个对象(被观察者)的状态变化会影响到其他多个对象(观察者)。

观察者模式的主要角色

  1. 被观察者(Subject):也称为主题或者可观察对象,它维护了一个观察者列表,可以添加、删除和通知观察者。当其状态发生变化时,会通知所有注册的观察者。
  2. 观察者(Observer):观察者是依赖于被观察者的对象,它定义了一个方法,用于在被观察者状态发生变化时进行更新操作。

观察者模式的工作流程

  1. 被观察者对象注册观察者:观察者通过某种方式向被观察者注册,通常是将自己添加到被观察者的观察者列表中。(建立起观察者与被观察者的关联)
  2. 被观察者状态变化
  3. 通知观察者:被观察者遍历观察者列表,调用每个观察者的更新方法(onEventhandle...)
  4. 观察者更新:每个观察者根据被观察者的通知进行相应的更新操作,执行与状态变化相关的任务

观察者模式的优点

解耦性:被观察者和观察者之间的关系是松耦合的,提高了代码的可维护性和扩展性。 可以轻松添加或删除观察者,可以在不修改被观察者的情况下增加新的观察者。

Spring 中的事件

Spring 的事件机制是 Spring 框架中的一个重要特性,基于观察者模式实现,它可以实现应用程序中的解耦,提高代码的可维护性和可扩展性。

Spring 的事件机制包括事件、事件发布、事件监听器等几个基本概念:

  1. 事件:事件是一个抽象的概念,它代表着应用程序中的某个动作或状态的发生。
  2. 事件发布:是事件发生的地方,它负责发布事件,从而通知事件监听器。
  3. 事件监听器:事件的接收者,它负责处理事件并执行相应的操作。

在 Spring 的事件机制中,事件源和事件监听器之间通过事件进行通信,从而实现了代码的解耦。

1

如上图所示,在观察者模式的实现中,往往还会有一个 Dispatcher 的角色, 由它来通知观察者,在 Spring 中,ApplicationContext 就扮演了这个角色。

如何定义事件

在 Spring 中,我们可以通过继承 ApplicationEvent 来自定义一个事件:

1
2
3
4
5
6
7
import org.springframework.context.ApplicationEvent;

public class MyEvent extends ApplicationEvent {
public MyEvent(Object source) {
super(source);
}
}

我们会发现,ApplicationEvent 有一个必选的参数 source,这个参数在实践中往往传递 this,也就是事件发生处的对象,这个参数不能为 null

如何监听事件?

在 Spring 中,监听事件的方式有两种:

  1. 实现 ApplicationListener,需要指定它要监听的事件类型
1
2
3
4
5
6
7
8
9
10
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
System.out.println("MyEventListener::onApplicationEvent");
}
}

注意:我们需要添加 @Component 注解以便 Spring 可以注册这个观察者。

  1. 使用 @EventListener 注解
1
2
3
4
5
6
7
8
9
10
11
import com.example.springeventdemo.event.MyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class SpringEventListener {
@EventListener
public void myEvent(MyEvent myEvent) {
System.out.println("my event.");
}
}

我们可以使用 @EventListener 注解在托管 bean 的任何方法上注册事件监听器。 需要监听的事件通过方法的参数来指定。

如何发布事件

我们可以使用 ApplicationEventPublisher 来发布一个事件,也就是通知所有的观察者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.example.springeventdemo.event.MyEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
public class MyService {
@Autowired
ApplicationEventPublisher applicationEventPublisher;

public void publish() {
applicationEventPublisher.publishEvent(new MyEvent(this));
}
}

也可以使用 ConfigurableApplicationContext,不过这个接口其实也是继承了 ApplicationEventPublisher 接口。

事件异步处理

有时候,我们的一些事件是可以异步处理的,比如注册成功之后给用户发送验证邮件, 注册成功我们就可以返回了,而发送验证邮件的这一步操作可以异步进行处理, 从而加快接口的响应速度。

在 Spring 中,我们可以使用 @Async 注解来将一个 EventListener 标记为异步处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class AsyncMyEventListener {
@EventListener
@Async
public void listen(MyEvent myEvent) {
try {
// 模拟耗时操作
Thread.sleep(1000);
// 请求结束之后才会输出下面这一行
System.out.println("AsyncMyEventListener::listen");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

但是,要使用 @Async 我们必须在我们的主程序类中加上 @EnableAsync 注解:

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync // 加上这个注解
public class SpringEventDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringEventDemoApplication.class, args);
}
}

使用 @Async 注解的监听器,会放到跟请求不同的线程中处理。

指定 EventListener 的处理条件

我们可以通过 EventListenercondition 属性来决定监听器是否需要执行:

使用 SpEL 表达式,当然我们也可以把判断写到方法体内。

1
2
3
4
5
// 在 myEvent 的 foo 属性等于 'bar' 的时候才会触发
@EventListener(condition = "#myEvent.foo == 'bar'")
public void myEvent1(MyEvent myEvent) {
System.out.println("my event: foo=bar");
}

使用 condition 而不是写到方法体中的原因是:

  1. 解耦和可配置性:通过将条件与事件监听器声明分离,你可以在不修改监听器代码的情况下更改条件。这使得在不同的环境或不同的配置下轻松切换监听器的行为成为可能。
  2. 动态切换行为:允许你根据应用程序或配置来动态决定是否触发事件监听器。这对于需要根据运行时条件来启动或禁用监听器的情况非常有用。
  3. 可测试性:可以为不同的条件编写单元测试,以确保条件的正确性。
  4. 统一管理:当有多个监听器时,将条件集中管理在 condition 属性中可以提高代码的可读性,因为你可以轻松查看每个监听器的条件而无需查看每个监听器的具体实现。

监听多个事件

我们可以通过 EventListenerclasses 属性来指定要监听的多个事件:

1
2
3
4
@EventListener(classes = {MyEvent.class, AnotherEvent.class})
public void myEvent2(Object event) {
System.out.println("myEvent2: " + event.getClass());
}

这个时候,我们的参数类型就需要修改一下了。

指定事件监听器的执行顺序

我们可以通过 @Order 注解来指定一个事件的不同监听器的执行顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;

@Component
public class SpringEventListener {
@Order(2)
@EventListener
public void myEvent2(MyEvent myEvent) {
// 后执行
System.out.println("my event. order = 2");
}

@Order(1)
@EventListener
public void myEvent1(MyEvent myEvent) {
// 先执行
System.out.println("my event. order = 1");
}
}

控制事件在事务提交前执行

有时候,我们会在代码中通过 @Transactional 来使用事务:

1
2
3
4
@Transactional(rollbackFor = RuntimeException.class)
public void saveFoo(Foo foo) {
fooRepository.save(foo);
}

假设我们在这个方法中有很多代码,然后其中穿插地发布了一些事件,但是我们希望这些事件在整个事务后才去触发监听器的处理逻辑, 这个时候我们就需要使用 @TransactionalEventListener 来注解我们的事件监听器,而不是使用 @EventListener

发布事件:

1
2
3
4
5
6
7
8
9
10
11
@Transactional(rollbackFor = RuntimeException.class)
public void saveFoo(Foo foo) {
// 发布了事件,但是事件处理器并不会马上处理,要等事务开始提交、结束提交的时候才会执行
// 所以我们会看到 "before save" 和 "after save" 输出在 "before commit" 之前
FooEvent event = new FooEvent(this);
eventPublisher.publishEvent(event);

System.out.println("before save");
fooRepository.save(foo);
System.out.println("after save");
}

事件监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
public class TransactionEventListener {
// 在事务提交前处理这个 FooEvent
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void beforeCommit(FooEvent event) {
System.out.println("before commit: foo event.");
}

// 在事务提交后处理这个 FooEvent
// 如果事务回滚则不会处理这个 FooEvent。
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void afterCommit(FooEvent event) {
System.out.println("after commit: foo event.");
}
}

上面的代码会输出:

1
2
3
4
before save
after save
before commit: foo event.
after commit: foo event.

@TransactionalEventListener 为我们提供了一个 phase 参数,让我们可以控制事件监听器的执行时机,它有以下可选值:

  • TransactionPhase.BEFORE_COMMIT:事务提交前
  • TransactionPhase.AFTER_COMMIT:事务提交后
  • TransactionPhase.AFTER_ROLLBACK:事务回滚后
  • TransactionPhase.AFTER_COMPLETION:事务完成后

tips:@TransactionalEventListener 并不是给我们监听事务的,只是控制事件在事务提交过程中的某一时刻触发。

什么是动态代理?

在 Java 中,动态代理是一种代理模式的实现方式,它允许在运行时动态地创建代理类并动态地处理方法调用。 动态代理常用于解耦合、处理和其他对象交互相关的行为控制。

动态代理例子

下面是一个简单的Java动态代理示例,它演示了如何使用动态代理创建一个代理对象,以及如何在代理对象上调用方法并在方法调用前后执行一些操作:

首先,我们需要创建一个代理接口,定义一些方法:

1
2
3
4
// 定义代理接口
public interface MyInterface {
void doSomething();
}

同时,也定义一个 MyInterface 的实现类:

1
2
3
4
5
6
public class MyInterfaceImpl implements MyInterface {
@Override
public void doSomething() {
System.out.println("MyInterfaceImpl doSomething");
}
}

然后,创建一个实现 InvocationHandler 接口的处理器类,它将负责在代理对象上调用方法时执行额外的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandler implements InvocationHandler {
private final Object realObject;

public MyInvocationHandler(Object realObject) {
this.realObject = realObject;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before invoking " + method.getName());

// 调用真实对象的方法
Object result = method.invoke(realObject, args);

System.out.println("After invoking " + method.getName());

return result;
}
}

接下来,我们可以在主程序中使用动态代理创建代理对象并调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.Proxy;

public class Main {
public static void main(String[] args) {
// 创建一个真实对象
MyInterface myInterface = new MyInterfaceImpl();

// 创建代理处理器
MyInvocationHandler handler = new MyInvocationHandler(myInterface);

// 创建代理对象
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class[]{MyInterface.class},
handler
);

// 调用代理对象的方法
proxy.doSomething();
}
}

在上述示例中,我们首先定义了一个代理接口 MyInterface ,然后创建了一个实现了 InvocationHandler 接口的处理器类 MyInvocationHandler, 该处理器负责在方法调用前后输出日志。最后,在主程序中,我们使用 Proxy.newProxyInstance 方法创建了代理对象,并通过代理对象调用了方法。在方法调用前后,会执行处理器中的操作。

以上代码会输出:

1
2
3
Before invoking doSomething
MyInterfaceImpl doSomething
After invoking doSomething

我们可以看到,虽然我们好像是在调用 MyInterface 接口的方法,但是实际上又不单纯是调用这个方法,还在调用这个方法前后做了一些事情。

我们可以通过下图来更加直观地了解上面这个动态代理的例子到底做了什么:

1

这个图体现了使用 Java 动态代理的一些关键:

  • 要使用 Java 中的 Proxy 来实现动态代理我们就需要定义 interface
  • 我们需要通过 InvocationHandler 来调用被代理对象的方法,在 InvocationHandlerinvoke 方法中,我们可以在调用被代理对象方法前后加一些自定义逻辑
  • 需要通过 Proxy.newProxyInstance 创建代理对象,这个代理对象实现了我们指定的接口(也就是第二个参数传递的 interface)。

这个示例演示了如何使用动态代理创建代理对象,并在方法调用前后执行自定义的操作。在实际应用中,你可以根据需要自定义处理器类,以执行不同的操作,如日志记录、性能监测、事务管理等。

动态代理的使用场景

动态代理在 Java 中是用得非常多的,使用动态代理我们可以在不侵入代码的情况下,为类增加或修改行为。常见的使用场景如下:

  1. RPC

在我们使用 Dubbo 的时候在我们服务端的类上加一个 @DubboService 注解就可以将一个类声明为可以被远程调用的服务了; 然后在客户端进行注入的时候,加上 @DubboReference 就可以将服务声明为一个远程服务了。

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
// 服务端
@DubboService(version = "${product.service.version}")
@Service
public class ProductServiceRpcImpl implements ProductService {
@Autowired
private IProductService productService;

@Override
public Sku test(Long id) {
return productService.test();
}
}

// 客户端
@Component
public class ProductClient {
@DubboReference(
version = "${product.service.version}",
cluster = "failfast",
timeout = 5000,
retries = 2,
proxy = "jdk" // 一般情况不需要加这个配置
)
private ProductService productService;

public void test() {
System.out.println(productService.test(1L));
}
}

在上面 Dubbo 客户端的代码中,我们在 @DubboReference 的注解中加了个 proxy = "jdk" 表示我们要使用 JDK 的动态代理,

但是我们需要知道的是,JDK 的动态代理性能上并不是最优的,所以 Dubbo 的默认代理方式不是 JDK 动态代理,上面指定 proxy 只是为了演示。

Dubbo 实现动态代理的方式有三种:bytebuddyjavassistjdk

我们可以通过下面代码验证一下上面的 ProductClient 是否真的使用了 JDK 的动态代理:

1
2
3
4
5
6
 public void test() {
// class com.sun.proxy.$Proxy123
System.out.println(productService.getClass());
// true
System.out.println(Proxy.isProxyClass(productService.getClass()));
}

从输出结果我们可以看到,productService 实际上就是 JDK 代理类的对象。

  1. Spring 容器

我们知道 Spring 容器可以帮助我们注入一些对象,而这些被注入的对象都是代理对象,直接 new 出来的对象跟 Spring 注入的对象是不一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
@SpringBootApplication
@MapperScan("com.baiguiren.mapper")
@RestController
public class Main {
@Autowired
private PersonService personService;

public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}

@GetMapping("/test")
public void test() {
// class com.sun.proxy.$Proxy66 这是一个代理类
System.out.println(personService.getClass());

PersonService personService1 = new PersonServiceImpl();
// class com.baiguiren.service.Impl.PersonServiceImpl,这是我们定义的类
System.out.println(personService1.getClass());
}
}

从上面这个例子我们发现,Spring 注入的对象,它的 Class 并不是我们所定义的那个类,而是一个代理类。

我的 aop 配置是 spring.aop.proxy-target-class=false,这样 Spring 会使用 JDK 代理,当然我们也可以去掉这个配置或者配置为 false,这样 Spring 会使用 CGLIB 来实现动态代理。

在使用 CGLIB 动态代理的时候,上面的 personService.getClass() 会输出 class (...).PersonServiceImpl$$EnhancerBySpringCGLIB$$23b7767d, 我们可以在输出中看到 $$EnhancerBySpringCGLIB$$,这就意味着我们拿到的注入的对象实际上是被 Spring 容器增强过的实例。

同样的,在我们使用 mybatisMapper 的时候,它也是一个代理:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class PersonServiceImpl implements PersonService {
@Autowired
private PersonMapper personMapper;

@Override
public Person find(int id) {
System.out.println(personMapper instanceof Proxy); // true
return personMapper.selectPersonById(id);
}
}

所以虽然我们定义了一个接口,里面什么内容也没写,但实际上底层会帮我们去实现一个类,然后基于这个类来创建实例。

  1. 事务管理

事实上,这也可以归到 2 中去,当然我们也可以在不使用 Spring 的情况下通过动态代理来实现事务管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
@Slf4j
public class FooServiceImpl implements FooService {
@Autowired
private JdbcTemplate jdbcTemplate;

@Override
@Transactional(rollbackFor = RollbackException.class) // 事务管理
public void insertThenRollback() throws RollbackException {
jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('BBB')");
throw new RollbackException();
}
}

这个例子中的 Transactional 其实也是 Spring 给我们提供的功能。通过 Transactional 注解,我们的 FooServiceImpl::insertThenRollback 会得到增强。

在我们抛出 RollbackException 的时候,这个方法中执行的操作将会被回滚,但是我们不需要手动去执行开启事务、提交事务、回滚事务的操作,比较方便。

上面的 insertThenRollback 转换为伪代码如下:

1
2
3
4
5
6
7
try {
beginTransaction();
jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('BBB')");
commit();
} catch (RollbackException e) {
rollback();
}

通过上面这几个场景,相信我们已经体会到了动态代理的强大之处了, 当然实际中的使用远不止上面这几种场景。

代理模式

我们在文章开头就说了,动态代理是一种代理模式的实现方式。

现在我们再来看看代理模式的定义:

在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

一个很简单的例子如下,我们需要针对控制器中的所有方法做性能监测:

1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/test1")
public void test1() throws InterruptedException {
long start = System.currentTimeMillis();

// 实际业务逻辑处理
System.out.println("test1...");
// 模拟耗时长的操作
Thread.sleep(100);

long end = System.currentTimeMillis();

log.info("GET /test1: " + (end - start)); // 输出日志
}

我们可以看到,上面这个例子实现的方式其实比较笨拙,如果我们有 100 个接口,上面的代码就得重复 100 遍,这种实现方式对开发人员是非常不友好的。

使用动态代理,我们可以有更加优雅的实现方式:

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
// 定义接口
public interface ITestController {
void test1();
}

// 定义实现类(被代理类)
public class TestController implements ITestController {
@Override
public void test1() {
System.out.println("test1...");
}
}

// 定义 InvocationHandler
@Slf4j
public class ControllerInvocationHandler implements InvocationHandler {
private final ITestController testController;

public ControllerInvocationHandler(ITestController testController) {
this.testController = testController;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();

// 调用被代理类的方法
Object result =method.invoke(testController, args);

long end = System.currentTimeMillis();
log.info(method.getName() + " invoked: " + (end - start)); // 输出日志

return result;
}
}

// 创建代理对象
ITestController testController = new TestController();
// 代理对象,它实现了所有的 testController.getClass().getInterfaces()
ITestController testControllerProxy = (ITestController) Proxy.newProxyInstance(
testController.getClass().getClassLoader(),
testController.getClass().getInterfaces(),
new ControllerInvocationHandler(testController)
);

// 调用代理对象的方法
testControllerProxy.test1();

当然,这也只是个例子,如果我们使用 Spring,我们可以通过 aspect 来实现上面这个功能。

同时,我们也发现,使用了动态代理之后,我们就不必在每一个方法中加上那些性能监测的代码了,这就可以将开发人员从繁琐的操作中解放出来了。

动态代理实现原理

了解了动态代理的基本使用、常用的使用场景之后,让我们再来了解一下它的实现原理(基于 JDK 1.8)。

还是以上一小节的例子来说明,在上面的例子中,我们为 ITestController 接口升成了一个代理对象,但是这个代理对象为什么可以直接调用被代理对象的方法呢?

这是因为,底层实际上是创建另一个继承自 java.lang.reflect.Proxy 的代理类,这个代理类实现了我们指定的 interfaces,而其中的方法体中,则会去调用实际被代理对象的方法。

这么说有点抽象,其实我们可以通过 Java 的一个命令行参数来让 Java 帮我们将升成的代理类保存到文件上:

1
2
// 注意:是 Java 8,新版本可能会有一些差异
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

对于上一个例子中的 ITestController 的代理类,我们可以看到它的内容实际如下(项目目录下的 com/sun/proxy 文件夹):

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
package com.sun.proxy;

// ...
// $Proxy69 继承了 java.lang.reflect.Proxy 这个类,
// 实现了 ITestController 接口

public final class $Proxy69 extends Proxy implements ITestController {
private static Method m3;
// ... 省略其他代码

public $Proxy69(InvocationHandler var1) throws {
super(var1);
}

public final void test1() throws {
try {
// 实际上调用了 InvocationHandler 的 invoke 方法
// this 也就是代理对象
// m3 是实际接口的 reflect.Method
// 实际调用的时候,没有传递任何参数
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

// ... 省略其他代码
static {
try {
// 也就是 ITestController 中 test1 的反射 Method
m3 = Class.forName("com.baiguiren.ITestController").getMethod("test1");
} ...
}
}

从生成的代码中,我们可以看到,所有对代理对象的方法调用,都委托给了 InvocationHandler::invoke 方法。

生成代理类的过程可以用下图表示(Proxy 类中的实现):

2
  1. Proxy 类中有两个方法会创建代理类,一个是 getProxyClass,另一个是 newProxyInstancegetProxyClass 获取 Class 对象,而 newProxyInstance 会基于这个代理类创建一个代理对象。
  2. 上一步的两个方法中都会调用 getProxyClass0 来获取 Class 对象,这个方法会有缓存,如果之前已经创建则会从缓存获取,否则会新建一个 Class
  3. 如果是第一次创建代理类,则会通过 ProxyClassFactory 来创建一个新的 Class
  4. 在实际加载到内存之前,会通过 ProxyGeneratorgenerateProxyClass 方法来生成 Class 对象的字节码
  5. 通过 defineClass0 方法来创建一个 Class 对象。(这是一个 native 方法,具体实现由 JVM 提供)。

拿到了代理类,也就是上面那一段 $Proxy69 类所表示的类之后,就可以创建代理对象了。

因为这个代理类实现了我们指定的接口,所以是可以通过类型转换转换成不同的 interface 的,如 (ITestController) Proxy.newProxyInstance(...)

最终,我们就拿到了一个实现了 ITestController 接口的代理对象,它的行为跟我们原来的对象完全一致,通过这种方式我们获得了一个附加了额外功能的代理对象。

其他实现动态代理的方式

除了使用 JDK 的动态代理之外,还有一种实现动态代理的方式是 CGLIB 动态代理,上面也有提及,他们都有一些局限性,需要根据我们的实际应用场景来选择。

比如,JDK 动态代理需要我们先定义 interface,而 CGLIB 是通过继承的方式来实现动态代理的,如果某个类有 final 标记,那它是无法使用 CGLIB 来做动态代理的。

本文主要是为了介绍动态代理的一些原理,至于其他代理方式,都是殊途同归的,所以本文不做过多探讨了。

总结

动态代理是一种 Java 编程技巧,通过在运行时生成代理类,它能够拦截对原始类的方法调用并在方法执行前后执行自定义操作, 这一特性使其广泛应用于解耦、性能监测、事务管理、RPC、AOP 等各种场景,提高了代码的可维护性和可扩展性。

最后,再简单总结一下本文的内容:

  • 动态代理是一种代理模式的实现方式,它允许在运行时动态地创建代理类并动态地处理方法调用。
  • 动态代理被广泛应用于 Spring 中,我们通过 @Autowired 注入的对象就是一个代理对象。
  • 动态代理还被用于 RPC、事务管理等,可以将非业务代码放到代理层去实现
  • JDK 的动态代理是在运行时才根据 ClassLoaderinterface 来创建 Class 的,具体加载到 JVM 的操作是底层帮我们实现的
  • 还有一种很常见的动态代理方式是 CGLIB 动态代理,但它不能代理 final 类。
  • 而使用 JDK 动态代理则需要先定义 interface,代理类会实现这些 interface,但是实际上还是会调用被代理对象的方法(通过 Invocationhandler)。

今天早上看到一篇文章《笨功夫是普通人最后的依靠》,有感而发,文中说的内容都是自己现在的一些想法, 本想在下面评论一下,但是好像要说的太多了,评论写不下,也就有了本文。

背单词是学英语的 “笨功夫”

故事还得从差不多十几年前的初中说起,在我上小学的时候,所在的小学是还没有教英语的,所以我的英语是从初中开始学的。 还好初中上英语课的时候,老师依然是从 26 个英语字母教起的,只是会有种错觉,那些小学学过英语的同学好像不用怎么学就能在英语上获得好成绩。 但这种想法不太客观,因为在有这种想法的时候,潜意识中将他们小学几年的积累忽略了,所以多少会有点沮丧。

九边的文章中,提到了学习英语得先背单词,背单词就是学习英语中的 “笨功夫”。对于这点深有体会,虽然我的英语是初中开始学的, 而且我对语法可以说知之甚少,但是我在背单词上可是花了不少时间的,所以英语成绩也不至于太难看。 后来上了大学,也能凭借初高中的英语单词的积累,考过了四级。 然后做为一名程序员,日常开发的时候需要看到英文文档也可以比较流畅,当然肯定不如看中文顺畅。 说这些,并不是觉得这是什么光荣的事,只是想表达比较认可背单词是学习英语的 “笨功夫” 这一观点。

笨功夫之外,方法也重要

几年前看极客时间的《数据结构与算法之美》这门课的时候,提到了一点:

数据结构和算法就是一个非常难啃的硬骨头,可以说是计算机学科中最难学的学科之一了。 我当时学习也费了老大的劲,能做到讲给你听,我靠的也是十年如一的积累和坚持。如果没有基础、或者基础不好,你怎能期望看 2 个小时就能完全掌握呢?

九边的文章中也提到了自己的经历,想学写代码,然后一推人推荐学习《算法导论》。 对于这个,我个人也是深受其害,十几年前我一直徘徊在玩游戏和学习之间,常常觉得自己的时间不应该全部还在玩游戏上,怎么也得学习一下。 然后我就会去学习,我怎么学习呢?也是跟九边一样,找那些前辈们推荐的经典教材,比如算法、操作系统、编译原理、计算机网络相关的经典书籍,

依然记得我在高三的时候就买来了一本《编译原理》,也就是那本 “龙书”(因为封面是一条龙)。 但是,这本编译原理就算让现在的我去看估计也很难看懂,所以在学习方面,个人其实走了很多弯路,跌了不少跟头。 因为学习的方法不对,这种学习自然是持续不下去的,在这种学习过程中,需要耗费大量的心力,然后自我怀疑,最后放弃。

对于这一点,九边的文章中也提到了背后的根本原因:

  • 选错教材了,你拿得太难了,不适合你;
  • 投入时间不足。

这两点都是我当时存在的问题,一上来就选择了最难的教材来看,没有考虑到自身实力能不能看得懂。 当然,选择难的教材也不是不行,可能得投入足够的时间去把教材看懂,前提是,有途径可以解决学习过程中的遇到的问题, 比如遇到问题可以问问前辈,又或者像现在的 GPT,如果想借助百度这种东西可能大概率要失望。 跳过少数问题可能不影响学习的效果,但是如果绝大部分问题都没有能找到答案的方法,那换一本简单点的教材先学学基础是个更好的方法。

当然,说这些并不是为了鼓励大家去学习数据结构算法,只是想说,在我们学习遇到困难的时候,可能得考虑一下是不是上面两个原因导致的。

大脑对熟悉的东西更感兴趣

这句话已经不记得是从哪里看到的了,但是觉得颇有道理。 有时候,我们对某件事情不感兴趣是因为不擅长,而不是因为真的不擅长。 在进入一个相对新的领域的时候,我们会接触到很多新的名词、术语,会觉得特别费劲。 这个时候我们可能就会选择放弃了,当然,我们也可以选择坚持下去, 这些年我学到的一个非常重要的学习方法就是,对于新接触的东西,去多看、多想、多实践几遍,当然,这里说的是学习编程领域的东西。 很多东西,在我们这样几轮学习下来,好像其实也没有太难的东西,当然也有可能我学习的东西本身没有很难的东西。 但不得不承认,就算是那些你觉得不难的东西,很多人也潜意识中会觉得自己学不会, 但实际上只是他们投入的时间还不够多。

在开始的时候,我也会觉得枯燥无味,但是在熟悉多几遍之后,发现好像还挺有意思,尤其是使用新学到的东西解决了自己实际遇到的一些问题之后。 学习的过程大多如此吧,说到这,想起了几天前看到的《如何取得杰出成就》中提到的一点:

有一些工作,我们可能必须在自己讨厌的事情上努力工作数年,才能接近喜欢的部分,但这不是杰出成就产生的方式, 杰出的成就是通过持续关注自己真正感兴趣的事情来实现的 —— 当我们停下来盘点时,会惊讶于自己已经走了多远。

这篇文章是《黑客与画家》作者博客《How to Do Great Work》的翻译版,有兴趣可以看看,感觉还不赖。 在实际工作中,我们遇到的很多问题其实并不需要坚持数年才能解决,又不是研究什么前沿领域的东西, 但是对于一些难题需要花几天或者一两个星期去解决这种可能性还是很大的。 在这个过程我们会对问题域中的东西越来越熟悉,直到最后它们不再是我们的障碍。

问题是可以分解的

搜狐 CEO 张朝阳这几年在 B 站上更新了很多物理的教程,当然我是全都看不懂,只是想起他说过的一段话:

很多东西的话,就是你看起来很复杂,是因为你不熟悉,其实这个知识, 天下的这个知识和所有的东西,其实都是不难的,你把所有的再复杂的东西,把它分解成每一步的话, 他的基本的这个思维过程的,跟你早上吃什么饭,怎么做饭,怎么打车怎么点东西,都是一样的思维过程。 很多东西你理解不了,不是因为你笨或者是你不够聪明,而是因为你,你自己认为你理解不了是吧, 很多可能因为过去的经历啊,就是在课堂上这个回答不了问题啊,一些挫败的经历,考试的失败导致, 你就有一种恐惧,是一种恐惧和你的认为理解不了导致你理解不了。

虽然道理是这么个道理,但是不代表物理都没学过的人能看得懂他讲的物理课, 因为问题虽然可以分解,但是一个需要有深厚基础的问题恐怕你连分解都不知道怎么分解,更不要提解决。 就好像上文提到的《算法导论》这本书,里面有大量的数学推导,很多人看不懂是因为, 从作者的角度来说,他已经把问题分解为了若干个小问题,在他看来,看这本书的读者应该是已经掌握了他分解之后的问题的相关知识。 从推荐看这本书的人来看,他推荐的对象应该也掌握了书中那些分解之后的知识。 但是实际是,可能也有很多人没有考虑到自身实力,然后就去啃这些大部头,自然而然地看不懂。

很多时候我们遇到的问题都能找到恰当的分解方法,尤其是编程领域,要不然我们不大可能会碰到那个问题。 在摸爬滚打多年之后,我们会发现,很多那些入行时觉得困难的点最后都不是问题了, 这是因为,常见的问题我们基本都解决过一遍了,以致于我们再遇到同样的问题之后,就能马上想到应该怎么去做,就已经在心中有一二三四几个步骤了。 举一个例子,在学习做 web 应用的时候,其实很多东西都不懂,但是现在已经很清楚一个 web 应用大概应该是长什么样子的了:

  • 从浏览器发起的请求到达 web 应用之后,我们需要确定具体执行什么逻辑,因此需要有 “路由” 来将请求拍发给一个具体的方法,也就是某个 Controller 里面的一个方法。
  • 在请求的处理逻辑里面,我们可能需要去查询数据库,所有常用的 web 框架都提供了关于数据库查询的一些抽象,直接调用封装的那些方法即可。
  • 在返回的时候,我们要返回给客户端的实质上是纯文本的东西(HTTP 协议),但是 HTTP 相关的功能往往由 HTTP 服务器来处理的,比如 nginx
  • nginx 处理 HTTP 相关的东西,比如反向代理的 upstream 返回的数据长度有多长,需要算出来,将这个长度写入到 HTTP 头中,这样浏览器收到 HTTP 报文的时候才能准确地解析出一个 HTTP 报文包

弄清楚这些问题之后,不管换哪一种语言,我们都可以拿来实现一个 web 应用,无非就是解析 HTTP 报文,在代码里面做一些业务逻辑处理,然后返回一串 HTTP 报文。 而这里提到的,其实就是针对 web 应用开发中的几个大问题的分解,这些问题对于写了几年 web 开发的人来说其实都不是问题了。

再举一个例子,对于程序员来说,我们往往需要持续地学习,当我们去学习一些新的编程语言的时候,我们可以去思考一下:对于编程语言来说,它可以分解为哪些问题? 个人感觉,这个问题其实挺有价值。要回答这个问题,我们可以回到没有今天这些高级编程语言的时候,那些计算机领域的先驱们是怎么让计算机工作起来的。 我们会发现,其实一开始他们是用的 0 和 1 来去写指令的,后面进化到汇编语言,毕竟一堆 0 和 1 谁记得住? 有了汇编,去做一些操作就简单多了,比如做加法,用一个 ADD 指令就可以了。 但是有了汇编之后,还有一个问题是,不管是从开发、维护上来说,都需要对 CPU 有非常清楚的了解,比如需要了解 CPU 有哪些寄存器之类的知识, 也就是说,使用汇编依然需要了解机器本身的很多运作机制,这无疑是一个比较高的门槛。 再后来到 C 语言的出现,我们就不需要了解 CPU 是怎么工作也可以写出能解决问题的代码了。 但是 C 语言依然有一个毕竟严重的问题,那就是需要开发者手动去申请内存,使用之后再释放内存,如果程序员忘记释放,那么就会造成内存的泄露。 所以更高级的一些语言就有了 GC,也就是说,由语言底层的运行时去帮程序员回收那些已经不再使用的对象。

扯得有点远了,回到问题本身,对于编程语言来说,它可以分解为哪些问题? 这个问题其实非常简单,只要我们随便找一门编程语言的教程来看它们的目录就会知道,一门编程语言本身包含了:

  • 一些基础语法:如代码组织结构是怎样的。Java 是通过一个个的类来组织的,Go 是通过结构体来建立抽象然后通过函数来进行组织的。
  • 对于面向对象的语言来说:不同的编程语言会有不同的类的编写方式。
  • 基本的变量定义是如何定义的
  • 关键字有哪些,比如非常常见的 classpublicdef 之类的
  • 如何实现循环结构
  • 如何实现条件判断
  • 如何在方法中返回值。有些语言需要使用 return,也有些语言比较省事,方法内的最后一行会被当做返回值,比如 ruby
  • 一些常用的数据结构是如何封装的。比如数组、map
  • 标准库。比如如何执行一个系统命令这种功能。
  • 其他...

这个清单不太完整,但是也足够了,因为编程语言的相似性,我们在熟悉了某一门编程语言之后,往往也会比较容易学会另一门编程语言。 但是这也并不代表,我们可以使用这门新的编程语言去解决一些实际的问题,除非,在此之前,我们已经使用了其他编程语言解决过相同的问题了。 比如,我们从 PHP 转换到 Go,我们在 PHP 中已经解决过很多数据库查询的问题了,切换到 Go 中,对于数据库查询的这一问题,我们可以作如下分解:

  • 找到 Go 中查询数据库相关的库
  • 调用库函数来建立到数据库的连接
  • 调用库函数来发送一个 SQL 查询语句到数据库服务器,然后等待数据库服务器返回查询结果
  • 取得查询结果,做其他处理

清楚了我们需要解决的问题之后,其实我们真正要解决的重要问题是如何组织我们的代码,从而使得我们针对这个问题的解决方案更好维护、能更好地使用。 所以现在在学习的时候,更喜欢从实际的问题出发(毕竟计算机领域其实是更偏向于实践)。 然后根据自己拆分后的问题去找解决方案,事实证明,这样效率更高。 如果我们从技术本身出发,我们可能无法知悉这个技术为什么是今天这个样子的,在这种学习方式之下, 我们新学习的东西无法跟我们脑子里原有的东西建立起连接,最终只会很快就忘记。 但是如果从我们熟悉的问题出发,去寻找一种新的解决方案的时候,其实新的知识跟自己的经验是可以很好的联系起来的,这样我们通过一个问题就能联系到不同的解决方案。

真的扯远了,说回正题。说这么多其实就是想说,碰到难题的时候我们也不能盲目地花笨功夫, 遇到难题的时候,我们也许可以考虑一下,这个问题可以如何分解,然后如何解决分解之后的问题。 如果分解后的问题是我们可以解决的,那我们的 “笨功夫” 那就是使用对了。

学习是为了找到学习方法

再说一个关于 “笨功夫” 的个人经历,还是初中的时候,在初中的时候花了很多时间在学习上,但是学习效果并不是非常明显, 多年以后,才明白,自己当初的那种学习其实是 “死学”,也就是不讲究方法的学习,免不了学了就忘。 初中的时候一个物理老师跟我们说他学生时代,有一天在思考问题很久之后突然 “开窍” 了, 以前没懂,现在知道了他说的 “开窍” 大概是找到了关于学习的套路。 可惜的是,我在读书的那十几年里,并没有经历过这样的 “开窍”,所以成绩一直平平无奇。

直到自己工作以后,因为自己从小到大是那种不太擅长交流的人,所以工作前几年遇到问题的时候也基本不会去请教别人, 那怎么办呢?那就自己想办法去解决各种技术问题呗,然后几年下来,好像自己的学习能力有所提升了,明显的表现是,学习新东西的时候会学习得更快了。 后面才懂,越来保持学习其实不只是为了学到各种解决问题的方法,实际上有很多东西都是学了之后用不上的,更重要的是在这个过程中学会如何学习。 关于这一点,陈皓有过一个经典的陈述。

学习不仅仅是为了找到答案,更是为了找到方法 - 陈皓

你有没有发现,在知识的领域也有阶层之分,那些长期在底层知识阶层的人,需要等着高层的人来喂养, 他们长期陷于各种谣言和不准确的信息环境中,于是就导致错误或幼稚的认知, 并习惯于那些不费劲儿的轻度学习方式,从而一点点地丧失了深度学习的独立思考能力,从而再也没有能力打破知识阶层的限制,被困在认知底层翻不了身。

可见深度学习十分重要,但应该怎样进行深度学习呢?下面有三个步骤:

  1. 知识采集。 信息源是非常重要的,获取信息源头、破解表面信息的内在本质、多方数据印证,是这个步骤的关键。
  2. 知识缝合。 所谓缝合就是把信息组织起来,成为结构体的知识。这里,连接记忆,逻辑推理,知识梳理是很重要的三部分。
  3. 技能转换。 通过举一反三、实践和练习,以及传授教导,把知识转化成自己的技能。这种技能可以让你进入更高的阶层。

这就好像,你要去登一座山,一种方法是通过别人修好的路爬上去,一种是通过自己的技能找到路(或是自己修一条路)爬上去。 也就是说,需要有路才爬得上山的人,和没有路能造路的人相比,后者的能力就会比前者大得多得多。 所以,学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力。

把时间当作朋友

这个标题来源于李笑来的《把时间当作朋友》这本书,书买了我还没看,但是看过他在得到的课程上这一话题的相关文章。

今天这个社会变得越来越浮躁,我们难免会受到影响,经常会想着今天做一件事,明天就能看到成果。 但实际上,在竞争激烈的今天,聪明人很多,又聪明又努力的也有很多,我们能做的只是接受这个事实, 然后持续在自己所在的领域花多一点 “笨功夫”,把时间当作朋友,就算最终我们没有实现最初的目标, 但是回头再看的时候,会发现原来自己已经走得很远了。

最后,用吴军《格局》中的一句话来结束本文:

事实上,功夫没下够,用什么方法都是在浪费时间。

0%