【Java】《OnJava8》笔记——第23章注解
都是个人学习过程的笔记,不是总结,没有参考价值,但是这本书很棒
基本语法
定义注解
注解通常会包含一些表示特定值的元素。当分析处理注解的时候,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,但是可以为其指定默认值。
例如
// annotations/UseCase.java
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
int id();
String description() default "no description";
}
元注解
Retention
注解的元素很重要,其中RUNTIME注解让我想起了spring的注解开发
RUNTIME:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。
编写注解处理器
// annotations/UseCaseTracker.java
import java.util.*;
import java.util.stream.*;
import java.lang.reflect.*;
public class UseCaseTracker {
public static void
trackUseCases(List<Integer> useCases, Class<?> cl) {
for(Method m : cl.getDeclaredMethods()) {
UseCase uc = m.getAnnotation(UseCase.class);
if(uc != null) {
System.out.println("Found Use Case " +
uc.id() + "\n " + uc.description());
useCases.remove(Integer.valueOf(uc.id()));
}
}
useCases.forEach(i ->
System.out.println("Missing use case " + i));
}
public static void main(String[] args) {
List<Integer> useCases = IntStream.range(47, 51)
.boxed().collect(Collectors.toList());
trackUseCases(useCases, PasswordUtils.class);
}
}
就是利用反射的方法获取类的所有的方法,然后对方法获取注解,我看了一下源码,方法对象获取注解的方法有getAnnotation(Class<T> annotationClass)
获取固定某一类的注解,如上面代码所示;getDeclaredAnnotations()
获取全部注解,返回Annotation
类的数组(这好像是所有注解的父类);以及getParameterAnnotations()
获取这个方法的参数的注解:Returns an array of arrays of Annotations that represent the annotations on the formal parameters, in declaration order
注意useCases.remove(Integer.valueOf(uc.id()));
这一行使用的是remove(Object m)
这个方法,即删除第一个出现的m
对象,useCases.remove(uc.id())
调用的是remove(int m)
即删除第m
个对象。
注解元素
默认值限制
首先,元素不能有不确定的值。也就是说,元素要么有默认值,要么就在使用注解时提供元素的值。
生成外部文件
对象/关系映射功能:就是Mybatis那种将javabean映射到数据库里的东西
替代方案
注解不支持继承
实现处理器
// annotations/database/TableCreator.java
// Reflection-based annotation processor
// {java annotations.database.TableCreator
// annotations.database.Member}
package annotations.database;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
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(
"No DBTable annotations in class " +
className);
continue;
}
String tableName = dbTable.name();
// If the name is empty, use the Class name:
if (tableName.length() < 1)
tableName = cl.getName().toUpperCase();
List<String> columnDefs = new ArrayList<>();
for (Field field : cl.getDeclaredFields()) {
String columnName = null;
Annotation[] anns =
field.getDeclaredAnnotations();
if (anns.length < 1)
continue; // Not a db table column
if (anns[0] instanceof SQLInteger) {
SQLInteger sInt = (SQLInteger) anns[0];
// Use field name if name not specified
if (sInt.name().length() < 1)
columnName = field.getName().toUpperCase();
else
columnName = sInt.name();
columnDefs.add(columnName + " INT" +
getConstraints(sInt.constraints()));
}
if (anns[0] instanceof SQLString) {
SQLString sString = (SQLString) anns[0];
// Use field name if name not specified.
if (sString.name().length() < 1)
columnName = field.getName().toUpperCase();
else
columnName = sString.name();
columnDefs.add(columnName + " VARCHAR(" +
sString.value() + ")" +
getConstraints(sString.constraints()));
}
StringBuilder createCommand = new StringBuilder(
"CREATE TABLE " + tableName + "(");
for (String columnDef : columnDefs)
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;
}
}
这段代码很好,不过就是需要手动吧Member
的全限定类名传入当参数就行,注意,获取字段的注解的时候使用的是getDeclaredAnnotations
方法,书上解释是:由于注解没有继承机制,如果要获取近似多态的行为,使用 getDeclaredAnnotations() 似乎是唯一的方式。
我不太理解是什么意思
使用javac处理注解
这一节主要讲的是元注解是@Retention(RetentionPolicy.SOURCE)
的注解如何处理,这种注解的元素在编译后,其注解信息会丢失
最简单的处理器
查看SimpleTest.class
反编译后的代码如下,发现其注解信息都被删掉了
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE, ElementType.METHOD,
ElementType.CONSTRUCTOR,
ElementType.ANNOTATION_TYPE,
ElementType.PACKAGE, ElementType.FIELD,
ElementType.LOCAL_VARIABLE})
public @interface Simple {
String value() default "-default-";
}
package chapter23.simplest;
// annotations/simplest/SimpleProcessor.java
// A bare-bones annotation processor
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import java.util.*;
@SupportedAnnotationTypes("chapter23.simplest.Simple")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SimpleProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
for (TypeElement t : annotations)
System.out.println(t);
for (Element el : env.getElementsAnnotatedWith(Simple.class))
display(el);
return false;
}
private void display(Element el) {
System.out.println("==== " + el + " ====");
System.out.println(el.getKind() +
" : " + el.getModifiers() +
" : " + el.getSimpleName() +
" : " + el.asType());
if (el.getKind().equals(ElementKind.CLASS)) {
TypeElement te = (TypeElement) el;
System.out.println(te.getQualifiedName());
System.out.println(te.getSuperclass());
System.out.println(te.getEnclosedElements());
}
if (el.getKind().equals(ElementKind.METHOD)) {
ExecutableElement ex = (ExecutableElement) el;
System.out.print(ex.getReturnType() + " ");
System.out.print(ex.getSimpleName() + "(");
System.out.println(ex.getParameters() + ")");
}
}
}
书上说需要调用javac -processor annotations.simplest.SimpleProcessor SimpleTest.java
这个代码,就会打印出结果了,我试了半天,也没有打印出来什么东西,空的@SupportedAnnotationTypes
和 @SupportedSourceVersion
分别用来指定注解的全限定类名以及源代码版本env.getElementsAnnotatedWith(Simple.class)
获取的是有Simple
注解的所有元素,并且打印这些元素的各种信息
诶诶诶调出来了调出来了,需要按顺序手动编译,而且需要进入src文件夹下面开始编译,idea生成的class文件在out文件夹里,和源文件是分开的,所以调试不出来,先编译Simple
类,因为其他类都依赖这个类,然后编译SimpleTest
类,最后编译SimpleTest
类
编译结果如下Element
类型对象是通过env.getElementsAnnotatedWith(Simple.class)
获得的,我理解就是挂载了Simple
注解的元素,他可能是方法,可能是类,可能是成员,然后再在display
方法里进行处理
同时,作者使用了向下转型
Element
只能执行那些编译器解析的所有基本对象共有的操作,而类和方法之类的东西有额外的信息需要提取。所以(如果你阅读了正确的文档,但是我没有在任何文档中找到——我不得不通过 * 寻找线索)你检查它是哪种ElementKind
,让后将其向下转换为更具体的元素类型,注入针对 CLASS 的TypeElement
和 针对 METHOD 的ExecutableElement
。此时,可以为这些元素调用其他方法。
看来有些问题真的只能靠口口相传解决
更复杂的处理器
这个函数的功能就是,抽取Multiplier
中的公共非静态方法,构建一个新的接口,并且通过@ExtractInterface
注解指定生成的接口的名称
具体细节书上讲解的挺好的,我不赘述了
基于注解的单元测试
依赖代码都在第二小节实现Unit里,然后由于我用的是idea,class文件都在另一个文件里,因此需要把class文件拷贝到源文件的地方,如下图所示
以src运行第一个测试样例如下,以src为根路径执行代码,哦要把AtUnitExample1.java编译了先
// annotations/AtUnitExample1.java
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/AtUnitExample1.class}
package annotations;
import onjava.atunit.*;
import onjava.*;
public class AtUnitExample1 {
public String methodOne() {
return "This is methodOne";
}
public int methodTwo() {
System.out.println("This is methodTwo");
return 2;
}
@Test
boolean methodOneTest() {
return methodOne().equals("This is methodOne");
}
@Test
boolean m2() { return methodTwo() == 2; }
@Test
private boolean m3() { return true; }
// Shows output for failure:
@Test
boolean failureTest() { return false; }
@Test
boolean anotherDisappointment() {
return false;
}
}
先不管@Test
是怎么实现的,至少从结果看,返回False
的结果就是失败的结果,其他的是成功的结果
// annotations/AUExternalTest.java
// Creating non-embedded tests
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/AUExternalTest.class}
package annotations;
import onjava.atunit.*;
import onjava.*;
public class AUExternalTest extends AtUnitExample1 {
@Test
boolean _MethodOne() {
return methodOne().equals("This is methodOne");
}
@Test
boolean _MethodTwo() {
return methodTwo() == 2;
}
}
上述代码的意义在于,我现在可以不用修改AtUnitExample1
的内部代码,就能实现单元测试了
// annotations/AtUnitExample2.java
// Assertions and exceptions can be used in @Tests
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/AtUnitExample2.class}
package annotations;
import java.io.*;
import onjava.atunit.*;
import onjava.*;
public class AtUnitExample2 {
public String methodOne() {
return "This is methodOne";
}
public int methodTwo() {
System.out.println("This is methodTwo");
return 2;
}
@Test
void assertExample() {
assert methodOne().equals("This is methodOne");
}
@Test
void assertFailureExample() {
assert 1 == 2: "What a surprise!";
}
@Test
void exceptionExample() throws IOException {
try(FileInputStream fis =
new FileInputStream("nofile.txt")) {} // Throws
}
@Test
boolean assertAndReturn() {
// Assertion with message:
assert methodTwo() == 2: "methodTwo must equal 2";
return methodOne().equals("This is methodOne");
}
}
上述代码说明了,方法返回void
的时候也可以测试,其实就是抛出异常就是失败,否则就是成功的测试
一个失败的 assert 或者从方法从抛出的异常都被视为测试失败,但是 @Unit 不会在这个失败的测试上卡住,它会继续运行,直到所有测试完毕
assert 1 == 2: "What a surprise!";
语句的意思是,如果判断条件为False
,则抛出一个java.lang.AssertionError("What a surprise!")
异常
// annotations/AtUnitExample4.java
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/AtUnitExample4.class}
// {VisuallyInspectOutput}
package annotations;
import java.util.*;
import onjava.atunit.*;
import onjava.*;
public class AtUnitExample4 {
static String theory = "All brontosauruses " +
"are thin at one end, much MUCH thicker in the " +
"middle, and then thin again at the far end.";
private String word;
private Random rand = new Random(); // Time-based seed
public AtUnitExample4(String word) {
this.word = word;
}
public String getWord() { return word; }
public String scrambleWord() {
List<Character> chars = Arrays.asList(
ConvertTo.boxed(word.toCharArray()));
Collections.shuffle(chars, rand);
StringBuilder result = new StringBuilder();
for(char ch : chars)
result.append(ch);
return result.toString();
}
@TestProperty
static List<String> input =
Arrays.asList(theory.split(" "));
@TestProperty
static Iterator<String> words = input.iterator();
@TestObjectCreate
static AtUnitExample4 create() {
if(words.hasNext())
return new AtUnitExample4(words.next());
else
return null;
}
@Test
boolean words() {
System.out.println("'" + getWord() + "'");
return getWord().equals("are");
}
@Test
boolean scramble1() {
// Use specific seed to get verifiable results:
rand = new Random(47);
System.out.println("'" + getWord() + "'");
String scrambled = scrambleWord();
System.out.println(scrambled);
return scrambled.equals("lAl");
}
@Test
boolean scramble2() {
rand = new Random(74);
System.out.println("'" + getWord() + "'");
String scrambled = scrambleWord();
System.out.println(scrambled);
return scrambled.equals("tsaeborornussu");
}
}
List<Character> chars = Arrays.asList(ConvertTo.boxed(word.toCharArray()));
里这个ConvertTo
是书提供的样例代码,我没找着在哪,所以我给改了一下,由于对闭包的支持,方法可以引用charArr
对象;另外我使用了mapToObj映射成对象流。这样写是因为我发现java8不支持CharStream,Stream.of
和Arrays.stream
也不接受char
数组
char[] charArr = word.toCharArray();
List<Character> chars = IntStream.range(0, word.length()).mapToObj(
i -> charArr[i]
).collect(Collectors.toList());
实现@Unit
AtUnit.java代码里有一段ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true);
,意思就是启用断言功能,因为默认是关闭的,具体见如何开启Java的断言?,这里的代码如果没有上述开启的代码,那么assert语句不会报错。
这些单元测试的代码非常好,可以捋一捋,是反射的非常优秀的案例代码
- 首先我们看之前运行测试样例的时候,在终端输入的都是像
java onjava.atunit.AtUnit chapter23/AtUnitExample4.class
的命令。通过观察AtUnit.java代码我们可以得知,实际上就是在以chapter23/AtUnitExample4.class
为参数,执行AtUnit
类的main
函数如下所示,最后那个if-else就是用来打印最终的测试结果的,而待测试方法的运行就是在new ProcessFiles(new AtUnit(), "class").start(args);
完成的
public static void main(String[] args) throws Exception {
ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true); // Enable assert
new ProcessFiles(new AtUnit(), "class").start(args);
if (failures == 0)
System.out.println("OK (" + testsRun + " tests)");
else {
System.out.println("(" + testsRun + " tests)");
System.out.println("\n>>> " + failures + " FAILURE" +
(failures > 1 ? "S" : "") + " <<<");
for (String failed : failedTests)
System.out.println(" " + failed);
}
}
- 既然是执行
main
函数,那就能用debug了啊!先来创建运行配置文件
然后设置运行参数,也就是class文件路径,注意这次要把src带上(这个文件是我自己编译的,正常情况下是在out文件夹下的)
然后就可以快乐地打断点debug了 - 回头来看代码
new ProcessFiles(new AtUnit(), "class").start(args);
实际上执行的核心就是AtUnit
的process
方法 -
ClassNameFinder.thisClass
就是分析字节码文件,里面的细节有兴趣可以看看,获取类名,例如public:chapter23.AtUnitExample4
,坑逼的地方是,代码里有一段Ignore unpackaged classes
,就是测试类必须得在包里头,也就是说类名里必须带点.
- 然后是通过
checkForCreatorMethod
获取creater
方法,通过checkForCleanupMethod
获取cleanup
方法 - 然后使用
createTestObject
调用构建方法获取待测试类的实例对象、 - 然后就是调用测试的方法了。
- 里面当然还有很多异常,比如待测试方法不能有参数啊,返回类型的要求啊什么的。
- InvocationTargetException is a checked exception that wraps an exception thrown by an invoked method or constructor.,这个异常代表着调用失败了。
- 这段代码是真JB好