本文是对 Java Annotation 基本知识的总结和梳理
Java annotation 官方说明
Annotations, a form of metadata ( "meta data" 我的理解是编程时就能确切知道的能够提供给运行时的信息), provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.
从上面定义的话大概体会是这么个意思:
Annotaion 是对目标代码的标签性注释,或者叫说明,用来给Annotation所标记的对象打上标签,就好比人的名字,某种意义来说就是标签。比如某场活动中要找出所有叫 “张三” 的人,这个时候 “张三” 这个标签就起到标识的作用了。
比如Spring 提供的 Annotation, @Autowired
, @Transaction
等表明了注解标注的对象在某种场景下会具有标识性。
Annotation 的用法
首先来看 Annotation 可以提供的一些比较重要的作用:
-
Information for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings. (为编译器提供有用的信息来发现错误或者忽略警告,例如
@SuppressWarnings
) -
Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth. (在编译和部署期间生成代码、XML文件等)
-
Runtime processing — Some annotations are available to be examined at runtime. (提供运行时的信息)
下面就来定义个简单的 Annotation 试试
:::Java
/**
* 定义一个用来生成代码的作者信息的 Annotation。 定义方法和
* Interface 类似,只是在 Interface 之前添加了一个 @ 符号
*/
public @interface Author {
String name();
String date();
}
/**
* 然后用定义好的类来修饰一个Class
*/
@Author(name="Falcon", date="2013-11-02")
public class HelloWorld {
@Author(name="Matt", date="2013-11-01")
public static String var = "";
@Author(name="Newton", date="2013-11-03")
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
这样就可以了,但是如果只标记 Class 而什么事都不做的话岂不是太无聊了。所以呢继续替 Autor 注解添加一个 jdk 预定义的 Annotation @Documented
, 这样就可以使用Javadoc 在生成的代码文档中添加 Author 的注解信息了。(没错,注解可以修饰注解,这叫原注解 - meta annotation)
@Documented
public @interface Author {
String name();
String date();
}
接下来用 Javadoc 生成文档看看。
javadoc org.cgfalcon.annotation
命令将生成 org.cgfalcon.annotation 包下所有源文件的文档。
先补个不加 @Documented
注解时生成的图
加了@Documented
注解的样子
Annotation 定义
Annotation - 注解:
注解类似被修饰对象的标签,可以在类(包括interface、enum)、方法、字段声明时使用。 你可以使用jdk 在 java.lang
和 java.lang.annotation
中定义的 Annotation,也可以像前面一样自己定义 Annotation。 定义的语法如下:
@interface <NameOfAnnotation> {
<TYPE> <FIELD_NAME>() [default <DEFAULT_VALUE>];
}
<TYPE> 可以是String, int, String[], Class<?> 等
<DEFAULT_VALUE> 给出字段的默认值
另外在 Java SE 8 中将扩展 Annotation 的使用范围,不但可以在声明时使用注解,而且可以在程序体中使用注解,这类注解叫 Type Annotation,例如
-
创建实例时使用注解
new @Interned MyObject();
-
类型转换
myString = (@NonNull String) str;
-
implements 子句
class UnmodifiableList<T> implements @Readonly List<@Readonly T> {...}
-
抛出异常时的声明
void monitorTemperature() throws @Critical TemperatureException {...}
Meta Annotation - 元注解:
Annotations that apply to other annotations are called meta-annotations.
也就是修饰注解的注解称为 元注解
Java SE 内置的 Annotation
JDK内置了三个预定义的Annotaion,分别是 @SuppressWarnings
, @Override
, @Deprecated
。除此之外还有一类 元注解, 就是之前说的 修饰注解的注解,就比如之前的 @Documented
一样
三个预定义注解
@Deprecated
, indicates that the marked element is deprecated and should no longer be used. (表示被标记的方法、类、字段不应当再使用了,可能会在未来的版本中删除)@Override
, informs the compiler that the element is meant to override an element declared in a superclass. (这个注解告诉编译器,被标记的对象将会覆盖父类中声明的相同的对象)@SuppressWarnings
, tells the compiler to suppress specific warnings that it would otherwise generate (这个注解告诉编译器,忽略被标记对象中可能存在的警告信息)
四个元注解(meta annotation)
-
@Target
, marks another annotation to restrict what kind of Java elements the annotation can be applied to. (表示注解可以在那些类型上使用) 可取的值有:- ElementType.ANNOTATION_TYPE
- ElementType.CONSTRUCTOR
- ElementType.FIELD
- ElementType.LOCAL_VARIABLE
- ElementType.METHOD
- ElementType.PACKAGE
- ElementType.PARAMETER
- ElementType.TYPE
-
@Retention
, specifies how the marked annotation is stored. (表示注解可以存在于哪些阶段)可取的值有:- RetentionPolicy.SOURCE, 源码级的注解,将会被编译器忽略
- RetentionPolicy.CLASS, 编译级注解,对编译器有用,但是会被JVM忽略
- RetentionPolicy.RUNTIME,运行时级注解,只有这类注解才能在运行时通过反射获取到
-
@Documented
, indicates that whenever the specified annotation is used those elements should be documented using the Javadoc tool. (告诉javadoc 是否需要在文档中生成) -
@Inherited
, indicates that the annotation type can be inherited from the super class. (This is not true by default.) (这个注解允许,当用户对当前 class 查询的注解不存在时,能够向该 class 的父类class查询该注解。另外,这个注解只能修饰 类) -
@Repeatable
, Java SE 8新增Annotatoin, 表明相同名称的注解可以出现超过1次
Annotation 行为的控制
Annotation 如果只是定义出来,放在 类,方法,字段上来起个简单的标签作用,那其实功能就大大减弱了。因此在自定义注解时,通常会定义和自定义Annotation 相匹配的一组行为来进行对代码增强。
下面的代码演示通过定义 Bean
,@Inject
Annotation 来完成实例注入。例子中定义 EmailValidator 是对Email地址的检测工具, EmailService 是对外提供的Email名称检测服务。
/**
* Bean Annotation. 被 @Bean 修饰的对象表示需要被容器加载
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {
}
/**
* @Inject 用来修饰字段,说明该字段需要由容器注入。
* @Inject 提供了属性 target 用来指定要注入的 Class
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
Class<?> target();
}
接下来是 Email服务 的定义
/**
* Email 实例,只提供 address字段
*/
public class Email {
private String address;
public Email(String address) {
this.address = address;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "Email{" +
"address='" + address + '\'' +
'}';
}
}
/**
* EmailValidator 提供对邮箱地址的检测功能
*/
@Bean
public class EmailValidator {
/**
* 使用正则表达式来检测邮箱地址是否合法
*/
private Pattern emailPattern = Pattern.compile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[\\w](?:[\\w-]*[\\w])?");
public boolean validate(Email email) {
if (email == null || email.getAddress() == null) {
return false;
}
Matcher matcher = emailPattern.matcher(email.getAddress());
if (matcher.find()) {
return true;
} else {
return false;
}
}
}
/**
* EmailService Bean,同样EmailService会由容器初始化并维护
*/
@Bean
public class EmailService {
/**
* 此处使用 @Inject Annotation,表明字段 emailValidator需要由容器注入
*/
@Inject(target = EmailValidator.class)
private EmailValidator emailValidator;
public void setEmailValidator(EmailValidator emailValidator) {
this.emailValidator = emailValidator;
}
public void validateEmail(Email email) {
if(emailValidator.validate(email)) {
System.out.println("Eamil valid");
} else {
System.out.println("Illegal Email Address Format");
}
}
}
这样 @Bean
, @Inject
就定义完了,使用也很简单,只需要在需要的字段或方法上加上注解即可。但是只标记不做解析的话就只能当花瓶了,根本毫无作用。因此我们还得自己定义对注解的解析逻辑。
下面的代码 BeanContext 实现一个简单的容器功能,主要做两个工作:
- 负责搜索指定 Package 下被
@Bean
标记的类并将之实例化 - 对于已经实例化的类,遍历其字段,查找出含有
@Inject
的字段,并使用合适的Class 实例给字段注入值。
代码如下:
/**
* BeanContext 充当一个简单Bean容器,负责查找 LOAD_PACKAGE 包下的类,加载
* 被 @Bean 修饰的类,然后对含有 @Inject 的字段进行注入
*/
public class BeanContext {
public static Map<Class<?>, Object> beanMap = new HashMap<Class<?>, Object>();
/**
* 加载类的起始包
*/
public static final String LOAD_PACKAGE = "org.cgfalcon.annotation";
static {
List<Class<?>> clazzList = new ArrayList<Class<?>>();
try {
Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(LOAD_PACKAGE.replace(".", "/"));
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
if (url != null) {
String protocol = url.getProtocol();
if (protocol.equals("file")) {
String cPath = url.getPath();
loadBeans(clazzList, cPath, LOAD_PACKAGE);
}
}
}
/**
* 将查找完的被 @Bean 修饰的类实例化放入 beanMap中
*/
for (Class<?> clazz : clazzList) {
Object instance = clazz.newInstance();
beanMap.put(clazz, instance);
}
/*
* Dependencies inject
*
*/
iocInject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 查找实例化好的实例,对其中需要 Inject 的字段注入合适的字段
*/
private static void iocInject() {
for (Map.Entry<Class<?>, Object> entry : beanMap.entrySet()) {
Class<?> clazz = entry.getKey();
Object instance = entry.getValue();
Field[] fields = clazz.getDeclaredFields();
if (fields != null && fields.length > 0) {
for (Field eachField : fields) {
if (eachField.isAnnotationPresent(Inject.class)) {
/* 在我的定义中 @Inject 包含一个字段 target, 用来制定要注入的实力类型,
* 此处通过该类型来从 beanMap中查找相应的实例
* */
Class<?> injectClass = eachField.getAnnotation(Inject.class).target();
Object injectInstance = beanMap.get(injectClass);
if (injectInstance != null) {
try {
/*
* 查找出来的实例注入instance 的指定字段中
*/
eachField.setAccessible(true);
eachField.set(instance, injectInstance);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
}
}
public static void loadBeans(List<Class<?>> clazzList, String packagePath, String loadPackage) {
File packageDir = new File(packagePath);
if (packageDir.isDirectory()) {
getSubpackageClasses(clazzList, packageDir, loadPackage);
}
}
/**
* 查找 packageDir 下的class文件,将含有 @Bean 注解的类取出来
*
* @param clazzList
* @param packageDir
* @param loadPackage
*/
private static void getSubpackageClasses(List<Class<?>> clazzList, File packageDir, String loadPackage) {
File[] subfiles = packageDir.listFiles();
for (File subFile : subfiles) {
if (subFile.isFile() && subFile.getName().endsWith(".class")) {
String fileName = subFile.getName();
String className = loadPackage + "." + fileName.substring(0, fileName.indexOf("."));
try {
Class<?> clazz = Class.forName(className);
if (clazz.isAnnotationPresent(Bean.class)) {
clazzList.add(clazz);
}
} catch (ClassNotFoundException e) {
System.out.printf("Failed to load class: <%s>\n", className);
}
} else if (subFile.isDirectory()) {
getSubpackageClasses(clazzList, subFile, loadPackage + "." + subFile.getName());
}
}
}
public static <T> T getBean(Class<T> clazz) {
if (beanMap == null) {
return null;
}
return (T) beanMap.get(clazz);
}
}
然后来实际使用看看
public class Main {
public static void main(String[] args) {
EmailService emailService = BeanContext.getBean(EmailService.class);
emailService.validateEmail(new Email("ddd"));
emailService.validateEmail(new Email("hotmail@gmail.com"));
}
}
输出结果为:
Email{address='ddd'} - illegal
Email{address='hotmail@gmail.com'} - valid
由上观之,自定义Annotation的使用通常需要经历: 自定义Annotation, 解析Annotation 两个步骤, 通常比较麻烦的地方就是在解析的步骤上。因此但凡说 Annotation,必定要涉及到使用的特定环境,假设脱离的语境,那么也就像花瓶一样,只能做摆设了。
博客链接: http://typeorigin.com
参考资料