java工具注释快捷键
在Java的早期版本中,开发人员只能在声明上编写注释。 使用Java 8,现在也可以在任何类型的使用上编写注释,例如声明,泛型和强制类型转换中的类型:
@Encrypted String data;
List< @NonNull String> strings;
myGraph = ( @Immutable Graph) tmpGraph;
乍一看,类型注释并不是新Java版本中最性感的功能。 确实,注释只是语法! -工具为注释提供语义(即含义和行为)。 本文介绍了新的类型注释语法和实用工具,以提高生产力并构建更高质量的软件。
在金融行业,我们不断变化的市场和监管环境意味着上市时间比以往任何时候都更为重要。 但是,不能选择牺牲安全性或质量:仅混淆百分比和基点可能会造成严重后果。 在所有其他行业中,同样的故事正在发挥作用。
作为Java程序员,您可能已经在使用注释来提高软件的质量。 考虑@Override注释,它是Java 1.5中引入的。 在具有非平凡的继承层次结构的大型项目中,很难跟踪在运行时将执行哪种方法的实现。 如果不小心,在修改方法声明时,可能会导致子类方法无法被调用。 以这种方式消除方法调用可能会引入缺陷或安全漏洞。 作为响应,引入了@Override批注,以便开发人员可以将方法记录为重写超类方法。 然后,如果程序与他们的意图不符,Java编译器将使用注释来警告开发人员。 通过这种方式,注释可作为计算机检查文档的一种形式。
注释还在通过元编程等技术提高开发人员的生产力方面发挥了核心作用。 这个想法是,注释可以告诉工具如何在运行时生成新代码,转换代码或行为。 例如,也在Java 1.5中引入的Java Persistence API(JPA),使开发人员可以使用声明中的注释(例如@Entity)以声明方式指定Java对象和数据库实体之间的对应关系。 Hibernate之类的工具使用这些批注在运行时生成映射文件和SQL查询。
对于JPA和Hibernate,注释用于支持DRY(请勿重复自己)原则。 有趣的是,无论您在哪里寻找支持开发最佳实践的工具,都很难找到注释! 一些著名的例子是减少与依赖注入的耦合,以及与面向方面的编程分离关注点。
这就提出了一个问题: 如果注释已经用于提高质量和提高生产率,为什么我们需要类型注释?
简短的答案是类型注释可以实现更多功能 -它们可以自动检测更多类型的缺陷,并让您更好地控制生产力工具。
类型注释语法
在Java 8中,可以在使用任何类型时编写类型注释,例如:
@Encrypted String data
List< @NonNull String> strings
MyGraph = ( @Immutable Graph) tmpGraph;
引入新的类型注释非常简单,只需使用ElementType.TYPE_PARAMETER目标, ElementType.TYPE_USE目标或同时使用这两个目标定义注释:
@Target({ElementType.TYPE_PARAMETER, ElementType. TYPE_USE })
public @interface Encrypted { }
ElementType.TYPE_PARAMETER目标指示可以将注释写在类型变量的声明上(例如, 类MyClass <T> {...} )。 ElementType.TYPE_USE目标指示可以在任何类型的使用(例如,在声明,泛型和强制类型转换中出现的类型)上编写注释。
一旦在源代码中对类型的批注(如声明的批注)进入源代码,它们既可以保存在类文件中,又可以在运行时通过反射(在批注定义上使用RetentionPolicy.CLASS或RetentionPolicy.RUNTIME策略)可用。 类型注释及其前身之间有两个主要区别。 首先,与声明注释不同, 局部变量声明类型的类型注释也可以保留在类文件中。 其次,完整的泛型类型将保留并在运行时可访问。
尽管注释可以存储在类文件中,但是注释不会影响程序的常规执行。 例如,开发人员可能在方法的主体中声明两个File变量和一个Connection变量:
File file = ...;@Encrypted File encryptedFile = ...;
@Open Connection connection = ...;
执行程序时,将这些文件中的任何一个传递到连接的send(...)方法将导致调用相同的方法实现:
// These lines call the same method
connection.send(file);
connection.send(encryptedFile);
如您所料,缺少运行时效果意味着,虽然可以对参数类型进行注释,但不能根据所注释的类型重载方法:
public class Connection{
void send(@Encrypted File file) { ... }
// Impossible:
// void send( File file) { ... }
. . .
}
这种局限性的直觉是,编译器没有任何方法来了解带注释的类型和未带注释的类型之间或具有不同注释的类型之间的关系。
可是等等! -有在相应于在方法签名的参数文件中的变量的encryptedFile注释@Encrypted; 但是方法签名中的注释与连接变量上的@Open注释对应的位置在哪里? 在调用connection.send(...)中 , 连接变量称为方法的“接收器”。 (“接收者”术语来自经典的面向对象的类比,即在对象之间传递消息)。 Java 8引入了一种用于方法声明的新语法,以便可以在方法的接收方上编写类型注释:
void send(@Open Connection this, @Encrypted File file)
同样,由于注释不影响执行,因此使用新的接收器参数语法声明的方法与使用传统语法的方法具有相同的行为。 实际上,当前新语法的唯一用途是使类型注释可以写在接收器的类型上。
可以在JSR(Java规范请求)308网站上找到类型注释语法的完整说明,包括多维数组的语法。
使用注释检测缺陷
在代码中编写注释可以强调错误代码中的错误:
@Closed Connection connection = ...;
File file = ...;
…
connection.send(file); // Bad!: closed and unencrypted!
但是,以上代码仍将编译,运行和崩溃-Java的编译器不会检查用户定义的注释。 相反,Java平台公开了两个API,即Java编译器插件和可插入批注处理API,以便第三方可以开发自己的分析。
在前面的示例中,注释实际上是对变量可以包含的值进行限定 。 我们可以想象出其他限定文件类型的方法: @Open File,@ Localized File,@ NonNull File ; 我们可以想象这些注释也可以限定其他类型,例如@Encrypted String 。 由于类型注释独立于Java类型系统,因此表示为注释的概念可以重新用于许多类型。
但是,如何自动检查这些注释? 直观上,某些注释是其他注释的子类型,可以对它们的使用进行类型检查。 考虑防止由数据库执行用户提供的(污染的)输入引起SQL注入攻击的问题。 我们可能认为数据是@Untainted或@MaybeTainted ,对应于数据是否保证不受用户输入的影响:
@MaybeTainted String userInput;
@Untainted String dbQuery;
@MaybeTainted批注可以被视为@Untainted批注的超类型。 有两种思考这种关系的方法。 首先, 可能被污染的值的集合必须是我们知道未被污染的值的超集(肯定未被污染的值可以是可能被污染的值的元素)。 相反,@Untainted注释提供比@MaybeTainted注释严格更强有力的保证。 因此,让我们看看我们的子类型直觉在实践中是否可行:
userInput = dbQuery; // OK
dbQuery = "SELECT FROM * WHERE " + userInput;// Type error!
第一行进行检查-如果我们假设未污染的值实际上是污染的,那我们就不会遇到麻烦。 我们的子类型化规则揭示了第二行的一个错误:我们正在尝试将超类型分配给限制性更强的子类型。
检查器框架
Checker Framework是用于检查Java批注的框架。 该框架于2007年首次发布,是一个活跃的开源项目,由JSR 308规范联合负责人Michael Ernst教授领导。 Checker Framework预先包装有各种注释和检查程序,用于检测缺陷,例如空指针取消引用,度量单位不匹配,安全漏洞以及线程/并发错误。 因为检查器在后台使用类型检查,所以结果是正确的-检查器不会遗漏任何可能的错误,在这种情况下,仅使用启发式工具的工具可能会出错。 框架使用编译器API在编译过程中运行这些检查。 作为框架,您还可以快速创建自己的注释检查器以检测特定于应用程序的缺陷。
该框架的目标是检测缺陷而不强迫您编写大量注释。 它主要通过两个功能实现此目的:智能默认设置和控制流敏感性。 例如,当检测到空指针缺陷时,检查器默认认为参数为非空。 检查器还可以使用条件语句来确定对表达式的引用是安全的。
void nullSafe(Object nonNullByDefault,@Nullable Object mightBeNull){
nonNullByDefault.hashCode(); // OK due to default
mightBeNull.hashCode(); // Warning!
if (mightBeNull != null){
mightBeBull.hashCode(); // OK due to check
}
}
实际上,默认值和控制流敏感性意味着您几乎不必在方法主体中编写注释-检查器可以自动推断和检查注释。 通过将注释的语义排除在官方Java编译器之外,Java团队确保了第三方工具的设计者和用户可以做出自己的设计决策。 这样可以进行自定义的错误检查,以满足项目的个性化需求。
定义自己的注释的能力还使您可以考虑使用特定于域的类型检查。 例如,在金融中,利率通常使用百分比来引用,而利率之间的差异通常使用基点(1%的1/100)来描述。 使用Checker Framework的Units Checker,您可以定义两个批注@Percent和@BasisPoints以确保您不会混淆这两个批注:
BigDecimal pct = returnsPct(...); // annotated to return @Percent
requiresBps(pct); // error: @BasisPoints is required
在这里,由于Checker Framework对控制流敏感,因此它基于两个事实知道在调用requireBps (pct)时pct是@Percent BigDecimal :首先,对returnPct(...)进行注释以返回a。 @Percent BigDecimal ; 其次,在调用requireBps(...)之前尚未重新分配pct 。 开发人员经常使用命名约定来尝试防止此类缺陷。 Checker Framework为您提供的保证是即使代码更改和增长,这些缺陷也不存在。
Checker框架已经在数百万行代码上运行 ,即使在经过良好测试的软件中也暴露出数百个缺陷。 也许是我最喜欢的例子:当框架在流行的Google Collections库(现在称为Google Guava)上运行时,它揭示了空指针缺陷,即使是广泛的测试和基于启发式的静态分析工具也没有。
这些结果是可以实现的,而不会使代码混乱。 实际上,使用Checker Framework验证属性每千行代码仅需要2-3个注释!
对于使用Java 6或Java 7的用户,您仍然可以使用Checker Framework来提高代码质量。 该框架支持以注释形式编写的类型注释(例如, / * @ NonNull * /字符串)。 从历史上看,其原因是Checker Framework与JSR 308(类型注释规范)于2006年开始共同开发。
虽然Checker框架是利用新语法进行错误检查的最佳框架,但它并不是目前唯一的框架。 Eclipse和IntelliJ都支持类型注释:
支持 | |
检查器框架 | 全面支持,包括注释中的注释 |
蚀 | 空错误分析支持 |
IntelliJ IDEA | 可以编写自定义检查器,不支持空错误分析 |
没有支持 | |
PMD | |
覆盖范围 | |
检查样式 | 不支持Java 8 |
查找错误 | 不支持Java 8 |
使用类型注释提高生产力
新型注释功能背后的主要驱动因素是错误检查。 也许并不奇怪,错误检查工具为注释提供了最佳的当前和计划支持。 但是,生产力工具中也有非常引人注目的应用程序,用于类型注释。 要了解原因,请考虑以下有关如何使用注释的示例:
面向方面的编程 | @ Aspect,@ Pointcut等 |
依赖注入 | @ Autowired,@ Inject等 |
坚持不懈 | @ Entity,@ Id等 |
注释是(1)工具应如何生成代码或辅助文件,以及(2)工具应如何影响程序的运行时行为的声明性规范。 以这些方式使用注释可以视为元编程。 一些框架(例如Lombok )采用带有注释的元编程,从而使代码看起来不再像Java了。
让我们首先考虑面向方面的编程(AOP)。 AOP旨在将诸如日志记录和身份验证之类的问题与程序的主要业务逻辑分开。 使用AOP,您可以在编译时运行一个工具,该工具会根据一组规则将其他代码添加到程序中。 例如,我们可以定义一个规则,该规则基于类型注释自动添加身份验证:
void showSecrets(@Authenticated User user){
// Automatically inserted using AOP:
if (!AuthHelper.EnsureAuth(user)) throw . . .;
}
和以前一样,注释限定了类型。 但是,不是在编译时检查注释,而是使用AOP框架在运行时自动执行验证。 此示例显示了用于使您对AOP框架修改程序的方式和时间有更多控制的类型注释。
Java 8还支持在本地声明中保留在类文件中的类型注释。 这为执行细粒度的AOP开辟了新的机会。 例如,以一种有规律的方式添加跟踪代码:
// Trace all calls made to the ar object
@Trace AuthorizationRequest ar = . . .;
同样,在使用AOP进行元编程时,类型注释可以提供更多控制。 依赖注入是一个类似的故事。 在Spring 4中,您最终可以将泛型用作限定符的一种形式:
@Autowired private Store<Product> s1;
@Autowired private Store<Service> s2;
使用泛型不再需要引入诸如ProductStore和ServiceStore之类的类,也无需使用基于名称的易碎注入规则。
使用类型注释,不难想象使用注释进一步控制注入(阅读:这在Spring中尚未实现):
@Autowired private Store<@Grocery Product> s3;
此示例演示了类型注释,该注释用作分离关注点的工具,可保持项目的类型层次结构整洁。 这种分离是可能的,因为类型注释独立于Java类型系统。
前方的路
我们已经看到了如何使用新类型的注释来检测/防止程序错误并提高生产率。 但是,类型注释的真正潜力在于将错误检查和元编程结合起来以启用新的开发范例。
基本思想是构建利用批注的运行时和库,以自动使程序更高效,并行或安全,并自动强制开发人员正确使用这些批注。
这种方法的一个很好的例子是Adrian Sampson的EnerJ框架,用于通过近似计算实现节能计算。 EnerJ基于这样的观察,有时,例如在移动设备上处理图像时,为了节省能源而需要权衡准确性。 使用EnerJ的开发人员使用@Approx类型注释来注释非关键数据。 基于这些注释,EnerJ运行时在处理该数据时会采用各种捷径。 例如,它可能会使用低能耗的近似硬件来存储数据并对其进行计算。 但是,让近似数据在程序中移动很危险---作为开发人员,您不希望控制流受到近似数据的影响。 因此,EnerJ使用Checker Framework强制没有近似数据可以流入控制流(例如,if语句)中使用的数据。
这种方法的应用不仅限于移动设备。 在金融领域,我们经常面临准确性与速度之间的权衡。 在这些情况下,可以根据当前需求和可用资源,保留运行时来控制Monte Carlo路径或收敛标准的数量,甚至在专用硬件上运行计算。
这种方法的好处是, 如何进行执行值得关注的是保持与核心业务逻辑分离描述执行什么计算。
结论
在Java 8中,除了能够在声明上编写注释之外,您还可以在使用任何类型时编写注释。 注释本身不会影响程序行为。 但是,通过使用诸如Checker Framework之类的工具,您可以使用类型注释自动检查和验证是否存在软件缺陷,并通过元编程提高生产率。 现有工具要充分利用类型注释会花费一些时间,现在是时候开始探索类型注释如何提高软件质量和生产率。
致谢
我感谢Michael Ernst,Werner Dietl和NYC Java Meetup对本文所基于的演示文稿提供了反馈。 我感谢Scott Bressler,Yaroslav Dvinov和Will Leslie审阅了本文的草稿。
java工具注释快捷键