欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Java】《OnJava8》笔记——第23章注解

程序员文章站 2024-03-05 12:59:12
...

都是个人学习过程的笔记,不是总结,没有参考价值,但是这本书很棒

基本语法

定义注解

注解通常会包含一些表示特定值的元素。当分析处理注解的时候,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,但是可以为其指定默认值。

例如

// 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反编译后的代码如下,发现其注解信息都被删掉了
【Java】《OnJava8》笔记——第23章注解


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这个代码,就会打印出结果了,我试了半天,也没有打印出来什么东西,空的
【Java】《OnJava8》笔记——第23章注解
@SupportedAnnotationTypes@SupportedSourceVersion分别用来指定注解的全限定类名以及源代码版本
env.getElementsAnnotatedWith(Simple.class)获取的是有Simple注解的所有元素,并且打印这些元素的各种信息
诶诶诶调出来了调出来了,需要按顺序手动编译,而且需要进入src文件夹下面开始编译,idea生成的class文件在out文件夹里,和源文件是分开的,所以调试不出来,先编译Simple类,因为其他类都依赖这个类,然后编译SimpleTest类,最后编译SimpleTest
【Java】《OnJava8》笔记——第23章注解
编译结果如下
【Java】《OnJava8》笔记——第23章注解
Element类型对象是通过env.getElementsAnnotatedWith(Simple.class)获得的,我理解就是挂载了Simple注解的元素,他可能是方法,可能是类,可能是成员,然后再在display方法里进行处理
同时,作者使用了向下转型

Element只能执行那些编译器解析的所有基本对象共有的操作,而类和方法之类的东西有额外的信息需要提取。所以(如果你阅读了正确的文档,但是我没有在任何文档中找到——我不得不通过 * 寻找线索)你检查它是哪种 ElementKind,让后将其向下转换为更具体的元素类型,注入针对 CLASS 的 TypeElement 和 针对 METHOD 的ExecutableElement。此时,可以为这些元素调用其他方法。

看来有些问题真的只能靠口口相传解决

更复杂的处理器

【Java】《OnJava8》笔记——第23章注解
【Java】《OnJava8》笔记——第23章注解
【Java】《OnJava8》笔记——第23章注解
这个函数的功能就是,抽取Multiplier中的公共非静态方法,构建一个新的接口,并且通过@ExtractInterface注解指定生成的接口的名称
具体细节书上讲解的挺好的,我不赘述了

基于注解的单元测试

依赖代码都在第二小节实现Unit里,然后由于我用的是idea,class文件都在另一个文件里,因此需要把class文件拷贝到源文件的地方,如下图所示
【Java】《OnJava8》笔记——第23章注解
以src运行第一个测试样例如下,以src为根路径执行代码,哦要把AtUnitExample1.java编译了先
【Java】《OnJava8》笔记——第23章注解

// 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.ofArrays.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语句不会报错。

这些单元测试的代码非常好,可以捋一捋,是反射的非常优秀的案例代码

  1. 首先我们看之前运行测试样例的时候,在终端输入的都是像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);
        }
}
  1. 既然是执行main函数,那就能用debug了啊!先来创建运行配置文件
    【Java】《OnJava8》笔记——第23章注解
    然后设置运行参数,也就是class文件路径,注意这次要把src带上(这个文件是我自己编译的,正常情况下是在out文件夹下的)
    【Java】《OnJava8》笔记——第23章注解
    然后就可以快乐地打断点debug了
  2. 回头来看代码 new ProcessFiles(new AtUnit(), "class").start(args);实际上执行的核心就是AtUnitprocess方法
  3. ClassNameFinder.thisClass就是分析字节码文件,里面的细节有兴趣可以看看,获取类名,例如public:chapter23.AtUnitExample4,坑逼的地方是,代码里有一段Ignore unpackaged classes,就是测试类必须得在包里头,也就是说类名里必须带点.
  4. 然后是通过checkForCreatorMethod获取creater方法,通过checkForCleanupMethod获取cleanup方法
  5. 然后使用createTestObject调用构建方法获取待测试类的实例对象、
  6. 然后就是调用测试的方法了。
  7. 里面当然还有很多异常,比如待测试方法不能有参数啊,返回类型的要求啊什么的。
  8. InvocationTargetException is a checked exception that wraps an exception thrown by an invoked method or constructor.,这个异常代表着调用失败了。
  9. 这段代码是真JB好
相关标签: Java