天天看点

Java注解处理器使用详解

在这篇文章中,我将阐述怎样写一个注解处理器(annotation processor)。在这篇教程中,首先,我将向您解释什么是注解器,你可以利用这个强大的工具做什么以及不能做什么;然后,我将一步一步实现一个简单的注解器。

一些基本概念

在开始之前,我们首先申明一个非常重要的问题:我们并不讨论那些在运行时(runtime)通过反射机制运行处理的注解,而是讨论在编译时(compile time)处理的注解。

注解处理器是一个在javac中的,用来编译时扫描和处理的注解的工具。你可以为特定的注解,注册你自己的注解处理器。到这里,我假设你已经知道什么是注解,并且知道怎么申明的一个注解类型。如果你不熟悉注解,你可以在这官方文档中得到更多信息。注解处理器在java 5开始就有了,但是从java 6(2006年12月发布)开始才有可用的api。过了一些时间,java世界才意识到注解处理器的强大作用,所以它到最近几年才流行起来。

一个注解的注解处理器,以java代码(或者编译过的字节码)作为输入,生成文件(通常是<code>.java</code>文件)作为输出。这具体的含义什么呢?你可以生成java代码!这些生成的java代码是在生成的.java文件中,所以你不能修改已经存在的java类,例如向已有的类中添加方法。这些生成的java文件,会同其他普通的手动编写的java源代码一样被javac编译。

虚处理器<code>abstractprocessor</code>

我们首先看一下处理器的api。每一个处理器都是继承于<code>abstractprocessor</code>,如下所示:

<code>init(processingenvironment env)</code>: 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的<code>init()</code>方法,它会被注解处理工具调用,并输入<code>processingenviroment</code>参数。<code>processingenviroment</code>提供很多有用的工具类<code>elements</code>, <code>types</code>和<code>filer</code>。后面我们将看到详细的内容。

<code>process(set&lt;? extends typeelement&gt; annotations, roundenvironment env)</code>: 这相当于每个处理器的主函数<code>main()</code>。你在这里写你的扫描、评估和处理注解的代码,以及生成java文件。输入参数<code>roundenviroment</code>,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。

<code>getsupportedannotationtypes()</code>: 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。

<code>getsupportedsourceversion()</code>: 用来指定你使用的java版本。通常这里返回<code>sourceversion.latestsupported()</code>。然而,如果你有足够的理由只支持java 6的话,你也可以返回<code>sourceversion.release_6</code>。我推荐你使用前者。

在java 7中,你也可以使用注解来代替<code>getsupportedannotationtypes()</code>和<code>getsupportedsourceversion()</code>,像这样:

因为兼容的原因,特别是针对android平台,我建议使用重载<code>getsupportedannotationtypes()</code>和<code>getsupportedsourceversion()</code>方法代替<code>@supportedannotationtypes</code>和<code>@supportedsourceversion</code>。

接下来的你必须知道的事情是,注解处理器是运行它自己的虚拟机jvm中。是的,你没有看错,javac启动一个完整java虚拟机来运行注解处理器。这对你意味着什么?你可以使用任何你在其他java应用中使用的的东西。使用guava。如果你愿意,你可以使用依赖注入工具,例如dagger或者其他你想要的类库。但是不要忘记,即使是一个很小的处理,你也要像其他java应用一样,注意算法效率,以及设计模式。如果你想学习java可以来这个群,首先是二二零,中间是一四二,最后是九零六,里面有大量的学习资料可以下载。

注册你的处理器

你可能会问,我怎样处理器<code>myprocessor</code>到javac中。你必须提供一个<code>.jar</code>文件。就像其他.jar文件一样,你打包你的注解处理器到此文件中。并且,在你的jar中,你需要打包一个特定的文件<code>javax.annotation.processing.processor</code>到<code>meta-inf/services</code>路径下。所以,你的.jar文件看起来就像下面这样:

Java注解处理器使用详解

打包进myprocessor.jar中的<code>javax.annotation.processing.processor</code>的内容是,注解处理器的合法的全名列表,每一个元素换行分割:

把<code>myprocessor.jar</code>放到你的builpath中,javac会自动检查和读取<code>javax.annotation.processing.processor</code>中的内容,并且注册<code>myprocessor</code>作为注解处理器。

例子:工厂模式

是时候来说一个实际的例子了。我们将使用maven工具来作为我们的编译系统和依赖管理工具。如果你不熟悉maven,不用担心,因为maven不是必须的。本例子的完成代码在github上。

开始之前,我必须说,要为这个教程找到一个需要用注解处理器解决的简单问题,实在并不容易。这里我们将实现一个非常简单的工厂模式(不是抽象工厂模式)。这将对注解处理器的api做一个非常简明的介绍。所以,这个问题的程序并不是那么有用,也不是一个真实世界的例子。所以在此申明,你将学习关于注解处理过程的相关内容,而不是设计模式。

我们将要解决的问题是:我们将实现一个披萨店,这个披萨店给消费者提供两种披萨(“margherita”和“calzone”)以及提拉米苏甜点(tiramisu)。

看一下如下的代码,不需要做任何更多的解释:

为了在我们的披萨店<code>pizzsstore</code>下订单,消费者需要输入餐(meal)的名字。

正如你所见,在<code>order()</code>方法中,我们有很多的<code>if</code>语句,并且如果我们每添加一种新的披萨,我们都要添加一条新的<code>if</code>语句。但是等一下,使用注解处理和工厂模式,我们可以让注解处理器来帮我们自动生成这些<code>if</code>语句。如此以来,我们期望的是如下的代码:

<code>mealfactory</code>应该是如下的样子:

<code>@factory</code>注解

你能猜到么:我们想用注解处理器自动生成<code>mealfactory</code>。更一般的说,我们将想要提供一个注解和一个处理器来生成工厂类。

我们先来看一下<code>@factory</code>注解:

想法是这样的:我们将使用同样的<code>type()</code>注解那些属于同一个工厂的类,并且用注解的<code>id()</code>做一个映射,例如从<code>"calzone"</code>映射到<code>"clzonepizza"</code>类。我们应用<code>@factory</code>注解到我们的类中,如下:

你可能会问你自己,我们是否可以只把<code>@factory</code>注解应用到我们的<code>meal</code>接口上?答案是,注解是不能继承的。一个类<code>class x</code>被注解,并不意味着它的子类<code>class y extends x</code>会自动被注解。在我们开始写处理器的代码之前,我们先规定如下一些规则:

只有类可以被<code>@factory</code>注解,因为接口或者抽象类并不能用<code>new</code>操作实例化;

被<code>@factory</code>注解的类,必须至少提供一个公开的默认构造器(即没有参数的构造函数)。否者我们没法实例化一个对象。

被<code>@factory</code>注解的类必须直接或者间接的继承于<code>type()</code>指定的类型;

具有相同的<code>type</code>的注解类,将被聚合在一起生成一个工厂类。这个生成的类使用factory后缀,例如<code>type = meal.class</code>,将生成<code>mealfactory</code>工厂类;

<code>id</code>只能是string类型,并且在同一个<code>type</code>组中必须唯一。

处理器

我将通过添加代码和一段解释的方法,一步一步的引导你来构建我们的处理器。省略号(<code>...</code>)表示省略那些已经讨论过的或者将在后面的步骤中讨论的代码,目的是为了让我们的代码有更好的可读性。正如我们前面说的,我们完整的代码可以在github上找到。好了,让我们来看一下我们的处理器类<code>factoryprocessor</code>的骨架:

你看到在代码的第一行是<code>@autoservice(processor.class)</code>,这是什么?这是一个其他注解处理器中引入的注解。<code>autoservice</code>注解处理器是google开发的,用来生成<code>meta-inf/services/javax.annotation.processing.processor</code>文件的。是的,你没有看错,我们可以在注解处理器中使用注解。非常方便,难道不是么?在<code>getsupportedannotationtypes()</code>中,我们指定本处理器将处理<code>@factory</code>注解。

elements和typemirrors

在<code>init()</code>中我们获得如下引用:

elements:一个用来处理<code>element</code>的工具类(后面将做详细说明);

types:一个用来处理<code>typemirror</code>的工具类(后面将做详细说明);

filer:正如这个名字所示,使用filer你可以创建文件。

在注解处理过程中,我们扫描所有的java源文件。源代码的每一个部分都是一个特定类型的<code>element</code>。换句话说:<code>element</code>代表程序的元素,例如包、类或者方法。每个<code>element</code>代表一个静态的、语言级别的构件。在下面的例子中,我们通过注释来说明这个:

你必须换个角度来看源代码,它只是结构化的文本,他不是可运行的。你可以想象它就像你将要去解析的xml文件一样(或者是编译器中抽象的语法树)。就像xml解释器一样,有一些类似dom的元素。你可以从一个元素导航到它的父或者子元素上。

举例来说,假如你有一个代表<code>public class foo</code>类的<code>typeelement</code>元素,你可以遍历它的孩子,如下:

正如你所见,element代表的是源代码。<code>typeelement</code>代表的是源代码中的类型元素,例如类。然而,<code>typeelement</code>并不包含类本身的信息。你可以从<code>typeelement</code>中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过<code>typemirror</code>获取。你可以通过调用<code>elements.astype()</code>获取元素的<code>typemirror</code>。

搜索@factory注解

我们来一步一步实现<code>process()</code>方法。首先,我们从搜索被注解了<code>@factory</code>的类开始:

这里并没有什么高深的技术。<code>roundenv.getelementsannotatedwith(factory.class))</code>返回所有被注解了<code>@factory</code>的元素的列表。你可能已经注意到,我们并没有说“所有被注解了<code>@factory</code>的类的列表”,因为它真的是返回<code>element</code>的列表。请记住:<code>element</code>可以是类、方法、变量等。所以,接下来,我们必须检查这些element是否是一个类:

为什么要这么做?我们要确保只有class元素被我们的处理器处理。前面我们已经学习到类是用<code>typeelement</code>表示。我们为什么不这样判断呢<code>if (! (annotatedelement instanceof typeelement) )</code>?这是错误的,因为接口(interface)类型也是typeelement。所以在注解处理器中,我们要避免使用<code>instanceof</code>,而是配合<code>typemirror</code>使用<code>ementkind</code>或者<code>typekind</code>。

错误处理

在<code>init()</code>中,我们也获得了一个<code>messager</code>对象的引用。<code>messager</code>提供给注解处理器一个报告错误、警告以及提示信息的途径。它不是注解处理器开发者的日志工具,而是用来写一些信息给使用此注解器的第三方开发者的。在官方文档中描述了消息的不同级别。非常重要的是<code>kind.error</code>,因为这种类型的信息用来表示我们的注解处理器处理失败了。很有可能是第三方开发者错误的使用了<code>@factory</code>注解(例如,给接口使用了@factory注解)。这个概念和传统的java应用有点不一样,在传统java应用中我们可能就抛出一个异常<code>exception</code>。如果你在<code>process()</code>中抛出一个异常,那么运行注解处理器的jvm将会崩溃(就像其他java应用一样),使用我们注解处理器factoryprocessor第三方开发者将会从javac中得到非常难懂的出错信息,因为它包含factoryprocessor的堆栈跟踪(stacktace)信息。因此,注解处理器就有一个<code>messager</code>类,它能够打印非常优美的错误信息。除此之外,你还可以连接到出错的元素。在像intellij这种现代的ide(集成开发环境)中,第三方开发者可以直接点击错误信息,ide将会直接跳转到第三方开发者项目的出错的源文件的相应的行。

我们重新回到<code>process()</code>方法的实现。如果遇到一个非类类型被注解<code>@factory</code>,我们发出一个出错信息:

让messager显示相关出错信息,更重要的是注解处理器程序必须完成运行而不崩溃,这就是为什么在调用<code>error()</code>后直接<code>return</code>。如果我们不直接返回,<code>process()</code>将继续运行,因为<code>messager.printmessage( diagnostic.kind.error)</code>不会停止此进程。因此,如果我们在打印错误信息以后不返回的话,我们很可能就会运行到一个nullpointerexception等。就像我们前面说的,如果我们继续运行<code>process()</code>,问题是如果在<code>process()</code>中抛出一个未处理的异常,javac将会打印出内部的nullpointerexception,而不是<code>messager</code>中的错误信息。

数据模型

在继续检查被注解@fractory的类是否满足我们上面说的5条规则之前,我们将介绍一个让我们更方便继续处理的数据结构。有时候,一个问题或者解释器看起来如此简单,以至于程序员倾向于用一个面向过程方式来写整个处理器。但是你知道吗?一个注解处理器任然是一个java程序,所以我们需要使用面向对象编程、接口、设计模式,以及任何你将在其他普通java程序中使用的技巧。

我们的<code>factoryprocessor</code>非常简单,但是我们仍然想要把一些信息存为对象。在<code>factoryannotatedclass</code>中,我们保存被注解类的数据,比如合法的类的名字,以及@factory注解本身的一些信息。所以,我们保存<code>typeelement</code>和处理过的@factory注解:

代码很多,但是最重要的部分是在构造函数中。其中你能找到如下的代码:

这里我们获取@factory注解,并且检查id是否为空?如果为空,我们将抛出illegalargumentexception异常。你可能感到疑惑的是,前面我们说了不要抛出异常,而是使用<code>messager</code>。这里仍然不矛盾。我们抛出内部的异常,你在将在后面看到会在<code>process()</code>中捕获这个异常。我这样做出于一下两个原因:

我想示意我们应该像普通的java程序一样编码。抛出和捕获异常是非常好的java编程实践;

如果我们想要在<code>factoryannotatedclass</code>中打印信息,我需要也传入<code>messager</code>对象,并且我们在错误处理一节中已经提到,为了打印<code>messager</code>信息,我们必须成功停止处理器运行。如果我们使用<code>messager</code>打印了错误信息,我们怎样告知<code>process()</code>出现了错误呢?最容易,并且我认为最直观的方式就是抛出一个异常,然后让<code>process()</code>捕获之。

接下来,我们将获取<code>@fractory</code>注解中的<code>type</code>成员。我们比较关心的是合法的全名:

这里有一点小麻烦,因为这里的类型是一个<code>java.lang.class</code>。这就意味着,他是一个真正的class对象。因为注解处理是在编译java源代码之前。如果你想学习java可以来这个群,首先是二二零,中间是一四二,最后是九零六,里面有大量的学习资料可以下载。我们需要考虑如下两种情况:

这个类已经被编译:这种情况是:如果第三方<code>.jar</code>包含已编译的被@factory注解<code>.class</code>文件。在这种情况下,我们可以想<code>try</code>中那块代码中所示直接获取<code>class</code>。

这个还没有被编译:这种情况是我们尝试编译被@fractory注解的源代码。这种情况下,直接获取class会抛出<code>mirroredtypeexception</code>异常。幸运的是,mirroredtypeexception包含一个<code>typemirror</code>,它表示我们未编译类。因为我们已经知道它必定是一个类类型(我们已经在前面检查过),我们可以直接强制转换为<code>declaredtype</code>,然后读取<code>typeelement</code>来获取合法的名字。

好了,我们现在还需要一个数据结构<code>factorygroupedclasses</code>,它将简单的组合所有的<code>factoryannotatedclasses</code>到一起。

正如你所见,这是一个基本的<code>map&lt;string, factoryannotatedclass&gt;</code>,这个映射表用来映射@factory.id()到factoryannotatedclass。我们选择<code>map</code>这个数据类型,是因为我们要确保每个id是唯一的,我们可以很容易通过map查找实现。<code>generatecode()</code>方法将被用来生成工厂类代码(将在后面讨论)。

匹配标准

我们继续实现<code>process()</code>方法。接下来我们想要检查被注解的类必须有只要一个公开的构造函数,不是抽象类,继承于特定的类型,以及是一个公开类:

我们这里添加了<code>isvalidclass()</code>方法,来检查是否我们所有的规则都被满足了:

必须是公开类:<code>classelement.getmodifiers().contains(modifier.public)</code>

必须是非抽象类:<code>classelement.getmodifiers().contains(modifier.abstract)</code>

必须是<code>@factoy.type()</code>指定的类型的子类或者接口的实现:首先我们使用<code>elementutils.gettypeelement(item.getqualifiedfactorygroupname())</code>创建一个传入的<code>class</code>(<code>@factoy.type()</code>)的元素。是的,你可以仅仅通过已知的合法类名来直接创建<code>typeelement</code>(使用typemirror)。接下来我们检查它是一个接口还是一个类:<code>superclasselement.getkind() == elementkind.interface</code>。所以我们这里有两种情况:如果是接口,就判断<code>classelement.getinterfaces().contains(superclasselement.astype())</code>;如果是类,我们就必须使用<code>currentclass.getsuperclass()</code>扫描继承层级。注意,整个检查也可以使用<code>typeutils.issubtype()</code>来实现。

类必须有一个公开的默认构造函数:我们遍历所有的闭元素<code>classelement.getenclosedelements()</code>,然后检查<code>elementkind.constructor</code>、<code>modifier.public</code>以及<code>constructorelement.getparameters().size() == 0</code>。

如果所有这些条件都满足,<code>isvalidclass()</code>返回<code>true</code>,否者就打印错误信息,并且返回<code>false</code>。

组合被注解的类

一旦我们检查<code>isvalidclass()</code>成功,我们将添加<code>factoryannotatedclass</code>到对应的<code>factorygroupedclasses</code>中,如下:

代码生成

我们已经手机了所有的被<code>@factory</code>注解的类保存到为<code>factoryannotatedclass</code>,并且组合到了<code>factorygroupedclasses</code>。现在我们将为每个工厂生成java文件了:

写java文件,和写其他普通文件没有什么两样。使用<code>filer</code>提供的<code>writer</code>对象,我们可以连接字符串来写我们生成的java代码。幸运的是,square公司(因为提供了许多非常优秀的开源项目二非常有名)给我们提供了<code>javawriter</code>,这是一个高级的生成java代码的库:

注意:因为javawriter非常非常的流行,所以很多处理器、库、工具都依赖于javawriter。如果你使用依赖管理工具,例如maven或者gradle,假如一个库依赖的javawriter的版本比其他的库新,这将会导致一些问题。所以我建议你直接拷贝重新打包javawiter到你的注解处理器代码中(实际它只是一个java文件)。

更新:javawrite现在已经被<code>javapoet</code>取代了。

处理循环

注解处理过程可能会多于一次。官方javadoc定义处理过程如下:

注解处理过程是一个有序的循环过程。在每次循环中,一个处理器可能被要求去处理那些在上一次循环中产生的源文件和类文件中的注解。第一次循环的输入是运行此工具的初始输入。这些初始输入,可以看成是虚拟的第0此的循环的输出。

一个简单的定义:一个处理循环是调用一个注解处理器的<code>process()</code>方法。对应到我们的工厂模式的例子中:<code>factoryprocessor</code>被初始化一次(不是每次循环都会新建处理器对象),然而,如果生成了新的源文件<code>process()</code>能够被调用多次。听起来有点奇怪不是么?原因是这样的,这些生成的文件中也可能包含@factory注解,它们还将会被<code>factoryprocessor</code>处理。

例如我们的<code>pizzastore</code>的例子中将会经过3次循环处理:

round

input

output

1

calzonepizza.javatiramisu.javamargheritapizza.javameal.javapizzastore.java

mealfactory.java

2

— none —

3

我解释处理循环还有另外一个原因。如果你看一下我们的<code>factoryprocessor</code>代码你就能注意到,我们收集数据和保存它们在一个私有的域中<code>map&lt;string, factorygroupedclasses&gt; factoryclasses</code>。在第一轮中,我们检测到了magheritapizza, calzonepizza和tiramisu,然后生成了mealfactory.java。在第二轮中把mealfactory作为输入。因为在mealfactory中没有检测到@factory注解,我们预期并没有错误,然而我们得到如下的信息:

这个问题是因为我们没有清除<code>factoryclasses</code>,这意味着,在第二轮的<code>process()</code>中,任然保存着第一轮的数据,并且会尝试生成在第一轮中已经生成的文件,从而导致这个错误的出现。在我们的这个场景中,我们知道只有在第一轮中检查<code>@factory</code>注解的类,所以我们可以简单的修复这个问题,如下:

我知道这有其他的方法来处理这个问题,例如我们也可以设置一个布尔值标签等。关键的点是:我们要记住注解处理过程是需要经过多轮处理的,并且你不能重载或者重新创建已经生成的源代码。

分离处理器和注解

如果你已经看了我们的代码库,你将发现我们组织我们的代码到两个maven模块中了。我们这么做是因为,我们想让我们的工厂模式的例子的使用者,在他们的工程中只编译注解,而包含处理器模块只是为了编译。有点晕?我们举个例子,如果我们只有一个包。如果另一个开发者想要把我们的工厂模式处理器用于他的项目中,他就必须包含<code>@factory</code>注解和整个<code>factoryprocessor</code>的代码(包括factoryannotatedclass和factorygroupedclasses)到他们项目中。我非常确定的是,他并不需要在他已经编译好的项目中包含处理器相关的代码。如果你是一个android的开发者,你肯定听说过65k个方法的限制(即在一个.dex文件中,只能寻址65000个方法)。如果你在factoryprocessor中使用guava,并且把注解和处理器打包在一个包中,这样的话,android apk安装包中不只是包含factoryprocessor的代码,而也包含了整个guava的代码。guava有大约20000个方法。所以分开注解和处理器是非常有意义的。

生成的类的实例化

你已经看到了,在这个<code>pizzastore</code>的例子中,生成了<code>mealfactory</code>类,它和其他手写的java类没有任何区别。进而,你需要就想其他java对象,手动实例化它:

如果你是一个android的开发者,你应该也非常熟悉一个叫做<code>butterknife</code>的注解处理器。在butterknife中,你使用<code>@injectview</code>注解android的view。butterknifeprocessor生成一个<code>myactivity$$viewinjector</code>,但是在butterknife你不需要手动调用<code>new myactivity$$viewinjector()</code>实例化一个butterknife注入的对象,而是使用<code>butterknife.inject(activity)</code>。butterknife内部使用反射机制来实例化<code>myactivity$$viewinjector()</code>对象:

但是反射机制不是很慢么,我们使用注解处理来生成本地代码,会不会导致很多的反射性能的问题?的确,反射机制的性能确实是一个问题。然而,它不需要手动去创建对象,确实提高了开发者的开发速度。butterknife中有一个哈希表hashmap来缓存实例化过的对象。所以<code>myactivity$$viewinjector</code>只是使用反射机制实例化一次,第二次需要<code>myactivity$$viewinjector</code>的时候,就直接冲哈希表中获得。

<code>fragmentargs</code>非常类似于butterknife。它使用反射机制来创建对象,而不需要开发者手动来做这些。fragmentargs在处理注解的时候生成一个特别的查找表类,它其实就是一种哈希表,所以整个fragmentargs库只是在第一次使用的时候,执行一次反射调用,一旦整个<code>class.forname()</code>的fragemnt的参数对象被创建,后面的都是本地代码运行了。

作为一个注解注解处理器的开发者,这些都由你来决定,为其他的注解器使用者,在反射和可用性上找到一个好的平衡。

总结

到此,我希望你对注解处理过程有一个非常深刻的理解。我必须再次说明一下:注解处理器是一个非常强大的工具,减少了很多无聊的代码的编写。我也想提醒的是,注解处理器可以做到比我上面提到的工厂模式的例子复杂很多的事情。例如,泛型的类型擦除,因为注解处理器是发生在类型擦除(type erasure)之前的(译者注:类型擦除可以参考这里)。就像你所看到的,你在写注解处理的时候,有两个普遍的问题你需要处理:第一问题, 如果你想在其他类中使用elementutils, typeutils和messager,你就必须把他们作为参数传进去。如果你想学习java可以来这个群,首先是二二零,中间是一四二,最后是九零六,里面有大量的学习资料可以下载。在我为android开发的注解器<code>annotatedadapter</code>中,我尝试使用dagger(一个依赖注入库)来解决这个问题。在这个简单的处理中使用它听起来有点过头了,但是它确实很好用;第二个问题,你必须做查询<code>elements</code>的操作。就想我之前提到的,处理element就解析xml或者html一样。对于html你可以是用jquery,如果在注解处理器中,有类似于jquery的库那那绝对是酷毙了。如果你知道有类似的库,请在下面的评论告诉我。

请注意的是,在factoryprocessor代码中有一些缺陷和陷阱。这些“错误”是我故意放进去的,是为了演示一些在开发过程中的常见错误(例如“attempt to recreate a file”)。如果你想基于factoryprocessor写你自己注解处理器,请不要直接拷贝粘贴这些陷阱过去,你应该从最开始就避免它们。