Java从 J2SE 5.0 引入注解, 如今注解已成为Java很重要的一部分. 我们都已在代码中见过@Override
和@Deprecated
这样的注解. 我将在这篇文章中讨论注解到底是什么, 为什么要引入注解, 注解怎样影响代码, 如何自定义注解(带示例代码), 注解的适用场景, 最后是ADF和注解.
注解是什么?
简单来说, 注解是元信息(metadata). 元信息是用来描述信息的信息. 所以注解是代码的元信息. 例如下面的代码
@Override
public String toString() {
return "This is String Representation of current object.";
}
我重写了 toString()
方法并且用上了@Override
注解. 就算我没有用@Override
, 这段代码也不会有任何问题. 那么使用注解的优势是什么呢? @Override
告诉编译器这个方法是一个重写的方法(方法的元信息), 如果父类中没有这个方法, 编译时就会抛出错误(method does not override a method from its super class). 现在如果我打字打错了, 写了这样一个方法toStrring() {double r}
, 代码将会正常的编译运行但输出和我预想的不一样的结果. 现在, 我们明白注解是什么了, 但去看一下正式定义仍然会有所裨益.
Annotation is a special kind of Java construct used to decorate a class, method, field, parameter, variable, constructor, or package. It’s the vehicle chosen by JSR-175 to provide metadata.
为什么要引入注解?
在注解出现前(甚至以后), XML被广泛用于描述元信息, 然后不知何故, 一部分开发者开始认为XML会给维护造成麻烦. 他们希望有一种和代码紧密结合的手段, 而不是和代码松耦合的XML(有时几乎时分离的). 如果你 google “XML vs. annotations”, 你会看到很多有趣的争论, 其中一个有趣的观点是引入XML配置是为了将配置和代码分离. 这些相反的观点可能会让你有些疑惑, 但两者各有利弊. 让我试着用一个例子来说明.
假如你有一些跨应用的常量/参数. XML会更适合这种场景, 因为它不依赖于任何代码. 如果你想将某些方法暴露成服务, 注解会是更好的选择, 因为它可以更紧密地和方法耦合开发者也应该清楚他们的关系.
另外也是很重要的一点是注解定义了一种在代码中定义元数据的标准. 在注解出现前, 人们会不同的方式定义元信息. 比如使用标记接口, 注释, transient关键字… 每个开发者都可能使用他自己的方式, 现在注解统一了这件事.
现在大多数框架既使用XML也使用注解以发挥两者的长处.
注解如何工作, 如何自定义注解
在开始这一节前, 我建议你看看这个Demo(AnnotationsSample.zip) , 这有助于你更好的理解下面的内容.
自定义注解非常简单. 你可以比较注解的定义和接口的定义. 来看看这两个例子— 一个是标准注解@Override
, 另一个是自定义注解 @Todo
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
上面的@Override
似乎有些奇怪; 它没有做任何事情 — 它只是检查一个方法是否在父类中定义过. 不要惊讶, 我没有开玩笑. Override注解的只有这点代码. 这是你需要理解的最重要的部分, 并且我要重申一遍: 注解只是元信息并且不含任何业务逻辑. 很难相信但是是真的. 如果注解不包含任何逻辑, 那么肯定有其他的某个地方做了些什么并且有某个地方是这个注解元信息的消费者. 注解只提供定义于其(类/方法/包/字段) 上属性的信息. 消费者是另外的一些取这些信息并做一些必要逻辑的代码.
当我们讨论像@Override
这样的标准注解时, JVM就是他们的消费者, 并且他们作用在字节层面. 这是应用开发者没办法控制也不能用于自定义的. 所以我们需要为我们自定义的注解编写消费者.
现在来看看这些用于自定义注解的关键字. 在之前的例子中你曾看到过有用于定义注解用到的注解.
J2SE 5.0 在java.lang.annotation 包中提供了四个仅用于定义注解使用的注解
@Documented
– 是否将它放到 JavaDoc中
@Retention
– 何时需要这个注解
@Target?
– 这个注解可以被放到哪些地方
@Inherited
– 子类是否可以读到这个注解.
@Documented
– 简单的标记是否要将注解加到Java doc中.
@Retention
– 定义这个注解要保留到哪一步.
-
RetentionPolicy.SOURCE
– 在编译期被抹去. 这些注解在编译完成后不会留下任何痕迹, 所以他们不会被写入字节. 例如@Override
,@SuppressWarnings
-
RetentionPolicy.CLASS
– 在类加载期间被抹去. 常用于字节层面处理, 令人不解的是, 这是默认的级别.(很少使用这种级别) -
RetentionPolicy.RUNTIME
– 不会被抹去. 这些注解可在运行时通过反射找到. 这是我们自定义注解常用的级别
@Target
– 注解可以被放在哪里. 如果你不声明, 它将可以放在任何地方. 以下是可选值. 很重要的一点是, 如果你想将注解放到下面7个地方, 只排除一个, 你需要显式声明7个.
ElementType.TYPE (class, interface, enum)
ElementType.FIELD (instance variable)
ElementType.METHOD
ElementType.PARAMETER
ElementType.CONSTRUCTOR
ElementType.LOCAL_VARIABLE
ElementType.ANNOTATION_TYPE (on another annotation)
ElementType.PACKAGE (remember package-info.java)
@Inherited
– 这个注解是否会影响子类.
现在清楚注解的定义由什么组成了吗? 注解只支持原生类型, String和枚举. 注解的所有属性都被定义成方法, 而且可以设置默认值.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Todo {
public enum Priority {LOW, MEDIUM, HIGH}
public enum Status {STARTED, NOT_STARTED}
String author() default "Yash";
Priority priority() default Priority.LOW;
Status status() default Status.NOT_STARTED;
}
下面是一个使用注解@Todo的例子:
@Todo(priority = Todo.Priority.MEDIUM, author = "Yashwant", status = Todo.Status.STARTED)
public void incompleteMethod1() {
//Some business logic is written
//But it’s not complete yet
}
如果注解里只有一个属性, 它就应该叫"value", 并且在使用它的时候不需要指定名称.
@interface Author{
String value();
}
@Author("Yashwant")
public void someMethod() {
}
现在一切都进展良好. 我们自定义了一个注解并添加了一些业务逻辑方法. 现在我们应该写一个消费者了. 为此需要用到反射, 反射可以提供Class, Method and Field对象, 这些对象都有一个可以返回注解的getAnnotation
方法, 我们要把它们转成自定义的注解(在instanceOf()
检查后), 然后就可以调用定义在注解上的方法了. 下面是一个简单的例子:
Class businessLogicClass = BusinessLogic.class;
for(Method method : businessLogicClass.getMethods()) {
Todo todoAnnotation = (Todo)method.getAnnotation(Todo.class);
if(todoAnnotation != null) {
System.out.println(" Method Name : " + method.getName());
System.out.println(" Author : " + todoAnnotation.author());
System.out.println(" Priority : " + todoAnnotation.priority());
System.out.println(" Status : " + todoAnnotation.status());
}
}
注解的使用场景
注解十分强大, 像Spring和Hibernat这样的框架中在日志和校验中广泛地使用了注解. 注解可以代替标记接口. 标记接口是控制整个类的行为, 而注解可以精确到方法, 例如是否要将某个特定的方法暴露成服务.
In the servlet specification 3.0, a lot of annotations were introduced, especially related to servlet security. Let’s check out a few:
servlet specification 3.0中引入了很多注解, 尤其是和servlet安全相关的, 比如以下几个:
-
HandlesTypes
– 声明ServletContainerInitializer
会使用到的类. -
HttpConstraint
– 代表应用于所有HTTP协议的请求的安全约束(没有使用HttpMethodConstraint
的) -
HttpMethodConstraint
– 代表应用于不同类型请求的安全约束, 通过在ServletSecurity
注解内使用该注解区分HTTP协议请求类型. -
MultipartConfig
– 用来标记接收multipart/form-data MIME 类型请求的Servlet -
ServletSecurity
– 在Servlet实现类中声明用来强制执行HTTP协议请求的安全约束. -
WebFilter
– 声明一个 Servlet Filter. -
WebInitParam
– 在WebFilter
或WebServlet
注解中声明一个Servlet或Filter的初始化参数. -
WebListener
— 在给定的web应用上下文中声明一个可监听多种类型事件的Listener -
WebServlet
– 声明配置为Servlet
ADF(Application Development Framework)和注解 [不重要]
ADF是我们要讨论的最后一部分, ADF是由Oracle开发, 用来构建Oracle Fusion Applications. 我们已经了解了使用框架的利弊, 现在我们知道怎么自定义注解, 但我们能在ADF中使用吗? ADF有提供原生注解吗?
这是个很有趣的问题: ADF中是否有对大规模使用注解的限制呢? 之前提到过的Spring, Hibernate, 使用了AOP(aspect-oriented-programming). 框架提供了一种可以在任何事件之前或之后注入代码的机制. ADF不使用AOP, 要使用注解可能要通过继承的方式了.
参考阅读
https://whatis.techtarget.com/definition/metadata
https://stackoverflow.com/questions/5971234/retentionpolicy-class-vs-runtime
https://www.baeldung.com/java-marker-interfaces