java编程思想之注解

注解 (元数据) 为咱们在代码中添加信息提供了一种形式化的方法,使咱们能够在稍后的某个时刻很是方便的使用这些数据。java

注解在必定程度上是在把元数据与源代码文件结合在一块儿,而不是保存在外部文档中。注解是众多引入 javaSE5 中的重要语言变化之一。他们能够提供用来完整地描述程序所需的信息,而这些信息是没法用 Java 来表达的。注解能够用来生成描述文件,甚至或是新的类定义,而且有助于编写减轻样板代码的负担。经过使用注解,咱们能够将这些元数据保存在 Java 源代码中,并利用 annotation API 为本身的注解构造处理工具,同时,注解的优势还包括:更加干净易读的代码以及编译期类型检查等。虽然 Java SE5 预先定义了一些元数据,但通常来讲,主要仍是须要程序员本身添加新的注解,而且按照本身的方式使用它们。程序员

注解的语法比较简单,除了 @ 符号的使用外,它基本与 Java 固有的语法同样。Java SE5 内置了三种,定义在 java.lang 中的注解:sql

  • @Override,表示当前的方法定义将覆盖超类中的方法。
  • @Deprecated,若是程序员使用了注解为它的元素,那么编译器会发出警告信息。
  • @SuppressWarnings,关闭不当的编译器警告信息。Java SE5 以前的版本也可使用这个注解,不过会被忽略不起做用。

每当你建立了描述性质的类或接口时,一旦其中包含了重复性的工做,那就能够考虑使用注解来简化与自动化该过程。注解是在实际的源代码级别保存全部的信息,而不是某种注释性的文字,这使得代码更整洁,且便于维护。经过使用扩展的 annotation API,或外部的字节码工具类库,程序员拥有对源代码以及字节码强大的检查与操做能力。数据库

基本语法

下面示例中,使用 @Test 对 TestExecute() 方法进行注解。这个注解自己并不作任何事情,可是编译器要确保在其构造路径上必须有 @Test注解的定义。api

public @interface Test {

}

public class Testble {
	public void execute() {
		System.out.println("Executing..");
	}

	@Test
	void testExecute(){
		execute();
	}
}

复制代码

被注解的方法与其余的方法没有区别。@Test 能够与任何修饰符共同做用域方法。数组

定义注解

上面的例子注解的定义咱们已经看到了。注解的定义看起来很像接口的定义。事实上与任何 Java 文件同样,注解也会被编译为 class 文件。除了 @ 符号之外,@Test 的定义很像一个空的接口。定义注解时会须要一些元注解,如 @Target@Retention@Target 用来定义你的注解将应用于什么地方。@Deprecated 用来定义应该用于哪个级别可用,在源代码中、类文件中或者运行时。bash

在注解中通常都会包含某些元素用以表示某些值。当分析出来注解时,程序和工具能够利用这些值。注解的元素看起来就像接口的方法,惟一的区别是你能够为他指定默认值。没有元素的注解被称为标记注解。app

下面是一个简单的注解,它能够跟踪一个项目中的用例。程序员能够在该方法上添加注解,咱们就能够计算有多少已经实现了该用例。框架

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
	public int id();
	public String description() default "没有描述";
}
复制代码

注意:id 和 description 相似方法的定义。description 元素有一个 default 值,若是在注解某个方法时没有给出 description 的值,则就会使用这个默认值。ide

下面的三个方法被注解:

public class PasswordUtils {
	@UseCase(id =47,description = "password 哈哈哈防止破解")
	public boolean validatePassword(String password) {
		return (password.matches("\\w*\\d\\w*"));
	}

	@UseCase(id = 48)
	public String encryptPassword(String password) {
		return new StringBuilder(password).reverse().toString();
	}

	@UseCase(id = 49,description = "是否包含在这个密码库中")
	public boolean checkForNewPassword(List<String> prevPassword,String password) {
		return !prevPassword.contains(password);
	}
}
复制代码

注解的元素在使用时是名值对的形式放入注解的括号内。

元注解

Java 目前只内置了三种标准注解,以及四种元注解。元注解就是注解的注解:

@Target 表示注解能够用在什么地方。ElementType 的参数包括:
CONSTRUCTOR:构造器的声明
FIELD:域声明
LOCAL_VARIABLE:局部变量声明
METHOD:方法声明
PACKAGE:包声明
PARAMETER:参数声明
TYPE:类、接口或enum声明
@Retention 表示须要在什么级别保存注解信息。可选的RententionPolicy参数:
SOURCE:注解将被编译器丢失
CLASS:注解在class文件中可用,但会被 vm 丢失
RUNTIME:vm 在运行期也保留注解,所以能够经过反射机制读取注解的信息。
@Documented 将此注解包含在 javaDoc 中
@Inherited 容许子类继承父类中的注解

大多数时候我么都是编写字节的注解,并编写本身的处理器处理他们。

编写注解处理器

若是没有用来读取注解的工具,那么注解就不会这有用。使用注解很重要的就是建立和使用注解处理器。Java SE5 扩展了反射机制的 API,方便咱们构造这种工具。同时还提供了一个外部工具 apt 帮助咱们解析带有注解的 Java 源代码。

下面咱们就用反射来作一个简单的注解处理器。咱们用它来读取上面的 PasswordUtils 类。

public class UseCaseTracker {

	public static void trackUseCase(List<Integer> useCase,Class<?> cl) {
		for (Method method : cl.getDeclaredMethods()) {
			UseCase uCase = method.getAnnotation(UseCase.class);
			if (uCase != null) {
				System.out.println("方法上的注解信息:"+uCase.id()+" "+uCase.description());
			}
		}

		for (Integer integer : useCase) {
			System.out.println("参数:"+integer);
		}
	}

	public static void main(String[] args) {
		List<Integer> uList = new ArrayList<>();
		Collections.addAll(uList, 47,48,49,50);
		trackUseCase(uList, PasswordUtils.class);
	}

}

复制代码

测试结果:

方法上的注解信息:49  是否包含在这个密码库中
方法上的注解信息:48  没有描述
方法上的注解信息:47  password 哈哈哈防止破解
参数:47
参数:48
参数:49
参数:50
复制代码

上面用到了两个反射的方法:getDeclaredMethods() 和 getAnnotation(),getAnnotation() 方法返回指定类型的注解对象,在这里使用 UseCse。若是被注解的方法上没有改类型的注解,则返回 null 值。而后咱们从返回的 UseCase 对象中提取元素的值。

注解元素

标签 @UseCase 由 UseCase,java 定义,包含 int 类型的元素 id,以及一个 String 类型的元素 description。注解元素可使用的类型包括:

  • 全部的基本类型
  • String
  • Class
  • enum
  • Annotation
  • 以上类型的数组

若是你使用了其余的类型,那么编译器会报错。注意也不容许使用任何包装类型,可是自动打包存在这也不是什么限制。注解也能够做为元素的类型,也就是说注解能够嵌套。

默认值限制

编译器对元素的默认值具备严格的限制。首先,元素不能有不肯定的值。也就是说元素必需要具备默认的值,要嘛在使用注解时提供元素的值。其次,对于非基本类型的元素,不管在源代码中声明时,或是在注解接口中定义时,都不能以 null 做为值。为了绕开这个限制,咱们只能定义一些特殊的值,例如;空字符串或者是负数表示某个元素不存在:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
	public int id() default -1;
	public String description() default " ";
}
复制代码

生成外部文件

假如咱们但愿提供一些基本的对象关系映射功能,可以自动生成数据库表,用以存储 Javabean 对象。能够选择使用 XML 描述文件。然而,若是使用注解的话,能够将全部的信息保存在 JavaBean 源文件中。为此咱们须要一个新的注解,用以定义与 Bean 关联的数据库表的名字,以及属性关联的列明和 SQL 类型。

下面是一个注解的示例,告诉注解处理器,咱们须要生成一个数据库表:

@Retention(RUNTIME)
@Target(TYPE)
public @interface DBtable {
	public String name() default "";
}

复制代码

注意:@Target 标签内能够有多个值用逗号分开,也能够没有值表示应用全部类型。其中的 name() 元素咱们用来为处理器建立数据库表提供名字。

接下来是修饰 javaBean 对象准备的注解:

@Retention(RUNTIME)
@Target(FIELD)
public @interface Constraints {
	boolean primaryKey() default false;
	boolean allowNull() default true;
	boolean unique() default false;
}

@Retention(RUNTIME)
@Target(FIELD)
public @interface SqlString {
	int value() default 0;
	String name() default "";
	Constraints constraints() default @Constraints;
}

@Retention(RUNTIME)
@Target(FIELD)
public @interface SQLInteger {
	String name() default "";
	Constraints constraints() default @Constraints;
}
复制代码

注解处理器经过 @Constraints 注解提取出数据库表的元数据。虽然对于数据库所能提供的全部约束而言只是一小部分,但足以表达咱们的思想。而且咱们也为三个元素提供了默认值。另外两个注解定义的是 SQL 类型。这些 sql 类型具备 name() 元素和 constraints() 元素。后者利用注解嵌套的功能将列的约束信息嵌入其中。咱们看到 @Constraints 注解类型以后没有指明元素的值而是用一个注解做为默认值。若是要让嵌入的 @Constraints 注解中的 unique() 元素为 true,并以此做为 constraints() 元素的默认值,则须要以下定义:

@Retention(RUNTIME)
@Target(FIELD)
public @interface SQLInteger {
	String name() default "";
	Constraints constraints() default @Constraints(unique = true);
}
复制代码

下面使咱们的 Bean 的示例:

@DBtable(name = "Member")
public class Member {
	@SqlString(30)
	String firstname;

	@SqlString(50)
	String lasttname;

	@SQLInteger
	Integer age;

	@SqlString(value = 30,constraints = @Constraints(primaryKey=true))
	String handle;

	static int menberCount;

	public String getFirstname() {
		return firstname;
	}

	public String getLasttname() {
		return lasttname;
	}

	public Integer getAge() {
		return age;
	}

	public String getHandle() {
		return handle;
	}

	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return handle;
	}
}

复制代码

类的注解 @DBTable 给定了值 MEMBER,他将会用来做为表的名字。Bean 的属性 firstname 和 lasttname ,都被注解为 @SqlString 类型,而且为其元素赋值为 30。咱们看到这其中使用了快捷方式。若是你的注解中定义了名为 value() 的元素,而且该元素在应用的时候是惟一须要赋值的元素。那么此时无需使用名值对的语法,只须要在括号内给出 value 的值便可。

注解不支持继承

不能使用关键字 extends 来继承某个 @interface。很遗憾若是注解支持继承的话能够大大减小咱们打字的工做量,而且使得语法更加整洁。

实现处理器

下面是一个注解处理器的示例,它将读取一个类文件,检查其上的数据库注解,并生成用来建立数据库的 SQL 命令:

public class TableCreator {

	public static void main(String[] args) throws Exception{
		 if(args.length < 1) {
		      System.out.println("arguments: annotated classes");
		      System.exit(0);
		  }
		for (String className : args) {
			Class<?> cl = Class.forName(className);
			DBtable dBtable = cl.getAnnotation(DBtable.class);
			if (dBtable != null) {
				System.out.println("这个类没哟建立数据库:"+className);
				continue;
			}

			//数据库的名字
			String tableName = dBtable.name();
			if (tableName.length()<1) {
				//若是名字没有赋值就用类名而且大写
				tableName = cl.getName().toUpperCase();
			}
			//查询出全部的列
			List<String> columnName = new ArrayList<>();
			for (Field field : cl.getDeclaredFields()) {
				String colum = null;
				//获取对象上的注解
				Annotation[] anns = field.getDeclaredAnnotations();
				if (anns.length <1) {
					continue;
				}

				if (anns[0] instanceof SQLInteger) {
					SQLInteger sInteger = (SQLInteger) anns[0];
					if (sInteger.name().length()<1) {
						colum = field.getName().toUpperCase();
					}else {
						colum = sInteger.name();
					}
					columnName.add(columnName + " INT" +getConstraints(sInteger.constraints()));
				}

				if(anns[0] instanceof SqlString) {
					SqlString sString = (SqlString) anns[0];
			          // Use field name if name not specified.
			          if(sString.name().length() < 1)
			        	  colum = field.getName().toUpperCase();
			          else
			        	  colum = sString.name();
			          columnName.add(columnName + " VARCHAR(" +
			            sString.value() + ")" +
			            getConstraints(sString.constraints()));
			      }

				 StringBuilder createCommand = new StringBuilder(
				          "CREATE TABLE " + tableName + "(");
				        for(String columnDef : columnName)
				          createCommand.append("\n " + columnDef + ",");
				        // Remove trailing comma
				        String tableCreate = createCommand.substring(
				          0, createCommand.length() - 1) + ");";
				        System.out.println("Table Creation SQL for " +
				          className + " is :\n" + tableCreate);
			}
		}

	}

	private static String getConstraints(Constraints con) {
	    String constraints = "";
	    if(!con.allowNull())
	      constraints += " NOT NULL";
	    if(con.primaryKey())
	      constraints += " PRIMARY KEY";
	    if(con.unique())
	      constraints += " UNIQUE";
	    return constraints;
	  }

}

复制代码

咱们使用注解来解析构造 sql 语句。上面的示例是很是简洁的一个例子。对于真正的对象影射数据库是很是复杂的。如今有不少这样的框架,能够将对象影射到关系数据库。好比:大名鼎鼎的 greenDAO。

使用 apt 处理注解

注解处理工具 apt,这是 sun 为了帮助注解处理的过程提供的工具。与 Javac 同样,apt 被设计为操做 Java 的源文件,而不是编译后的类。默认状况下 apt 会在处理完源文件后编译他们。当注解处理器生成一个新的源文件时,改文件会在新一轮的注解处理中接受检查。该工具会一轮一轮的处理,直到再也不有新的源文件产生。

咱们定义的每个注解都须要本身的处理器,而 apt 工具能够很容易的将多个处理器组合在一块儿。这样咱们就能够指定多个要处理的类。经过使用 AnnotationProcessorFactory,apt 可以为每个它发现的注解生成一个正确的注解处理器。使用 apt 生成注解处理器时,咱们没法利用 Java 的反射机制,由于咱们操做的是源代码,而不是编译后的类。使用 mirror API 可以解决这个问题,他使得咱们可以在未经编译的源代码中查看方法、对象以及类型。

下面是一个自定义的注解,使用它能够把一个类的 public 方法提取出来,构形成一个新的接口:

@Retention(SOURCE)
@Target(TYPE)
public @interface ExtractInterface {
	public String value();
}
复制代码

咱们看到 @Retention(SOURCE) 是 SOURCE。由于咱们从使用了该注解的类抽取接口以后不必在保留这些注解信息。下面的类有一个公共的方法,咱们将把他抽取到一个接口中:

@ExtractInterface("Multiplier")
public class Multiplier {

	public int multiply(int x,int y) {
		int total = 0;
		for (int i = 0; i < x; i++) {
			total = add(total, y);
		}
		return total;
	}

	public int add(int x,int y) {
		return x+y;
	}

	public static void main(String[] args) {
		Multiplier multiplier = new Multiplier();
		System.out.println("11*16=" + multiplier.multiply(11,16));

	}

}

复制代码

测试结果:

11*16=176
复制代码

在 Multiplier 类中有一个 multiply() 方法,该方法通过循环调用私有的 add() 方法实现乘法操做。add() 方法不是公共的,所以不将其做为接口的一部分。注解给了类名做为值,这就是将要生成的接口的名字:

import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.MethodDeclaration;
import com.sun.mirror.declaration.Modifier;
import com.sun.mirror.declaration.ParameterDeclaration;
import com.sun.mirror.declaration.TypeDeclaration;

import genericity.New;

public class InterfaceExtractorProcessor implements AnnotationProcessor{

	private final AnnotationProcessorEnvironment aenv;

	private ArrayList<MethodDeclaration> interfaceMethods = new ArrayList<>();



	protected InterfaceExtractorProcessor(AnnotationProcessorEnvironment aenv) {
		super();
		this.aenv = aenv;
	}

	@Override
	public void process() {
		for (TypeDeclaration typeeclaration : aenv.getSpecifiedTypeDeclarations()) {
			ExtractInterface annot = typeeclaration.getAnnotation(ExtractInterface.class);
			if (annot == null) {
				break;
			}

			for (MethodDeclaration methodDeclaration : typeeclaration.getMethods()) {
				if (methodDeclaration.getModifiers().contains(Modifier.PUBLIC) && !(methodDeclaration.getModifiers().contains(Modifier.STATIC))) {
					interfaceMethods.add(methodDeclaration);
				}
			}

			if (interfaceMethods.size() >0) {
				try {
					PrintWriter writer = aenv.getFiler().createSourceFile(annot.value());
					 writer.println("package " +
					 typeeclaration.getPackage().getQualifiedName() +";");
					 writer.println("public interface " +
					 annot.value() + " {");
					 for(MethodDeclaration m : interfaceMethods) {
				            writer.print(" public ");
				            writer.print(m.getReturnType() + " ");
				            writer.print(m.getSimpleName() + " (");
				            int i = 0;
				            for(ParameterDeclaration parm :
				              m.getParameters()) {
				              writer.print(parm.getType() + " " +
				                parm.getSimpleName());
				              if(++i < m.getParameters().size())
				                writer.print(", ");
				            }
				            writer.println(");");
				          }
				          writer.println("}");
				          writer.close();

				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}

			}
		}
	}

}

复制代码

程序中用到的 mirror 的 jar 包能够到下面的地址下载:

http://www.java2s.com/Code/Jar/a/Downloadaptmirrorapi01jar.htm

全部的工做都在 process() 中完成。在分析一个类的时候,咱们用 MethodDeclaration 类以及其上的 getModifiers() 方法找到 public 方法。一旦找到就将其保存到一个 ArrayList 中。而后在一个 .java 文件中建立新的接口中的方法定义。

注意在构造器中以 AnnotationProcessorEnvironment 对象为参数。经过该对象咱们知道 apt 正在处理的全部类型,而且能够经过他获取 Messager 对象和 Filer 对象。Filer 对象是一种 PrintWriter,咱们能够经过他建立新的文件。不使用普通的 PrintWriter 而是使用 Filer 对象主要缘由是:只有这样 apt 才知道咱们建立的新文件,从而对新文件进行注解处理,而且在须要的时候编译他们。

createSourceFile() 方法以将要新建的类或接口名字,打开了一个普通的输出流。

apt 工具须要一个工厂类来为其指明正确的处理器,而后它才能调用处理器上的 process() 方法:

public class InterfaceExtractorProcessorFactor implements AnnotationProcessorFactory{

	@Override
	public AnnotationProcessor getProcessorFor(Set<AnnotationTypeDeclaration> arg0, AnnotationProcessorEnvironment arg1) {

		return new InterfaceExtractorProcessor(arg1);
	}

	@Override
	public Collection<String> supportedAnnotationTypes() {
		// TODO Auto-generated method stub
		return Collections.singleton("annotations.ExtractInterface");
	}

	@Override
	public Collection<String> supportedOptions() {
		// TODO Auto-generated method stub
		return Collections.emptySet();
	}

}

复制代码

AnnotationProcessorFactory 接口只有三个方法。其中 getProcessorFor() 方法注解处理器,该方法包含类型声明的 Set 以及 AnnotationProcessorEnvironment 对象做为参数。另外两个方法是 supportedAnnotationTypes() 和 supportedOptions(),能够经过他们检查一下是否 apt 工具发现的全部的注解都有相应的处理器,是否全部控制台输入的参数都是你提供的可选项。

若有疑问,能够关注我。

相关文章
相关标签/搜索