Java编程思想 第二十章 注解

20.0 前言

注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,是我们可以在稍后某个时刻非常方便地使用这些数据。注解在一定程度上是把元数据与源代码文件结合在一起,而不是保存在外部文档中。

JSE5内置的注解:
@Overrided
@Deprecated 提示不建议使用
@Suppress Warnings 忽略不当的编译器警告信息

20.1 基本语法

20.1.1 定义注解

注解的定义很像接口的定义,和接口一样,注解也会编译成class文件。
注解中会定义一些元素以表示某些值。注解的元素看起来像接口的方法,唯一的区别是可以为其指定默认值。

package com.fei.hanhanhan;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)   // 可以用到什么地方
@Retention(RetentionPolicy.RUNTIME)    // 在什么级别保存注解信息
public @interface UseCase {
	// 元素的类型:所有基本类型,String,Class,enum,Annotation,以上类型的数组
	// 元素的值不可以为null
    public int id() default -1;     
    public String description() default "";  
}

20.1.2 元注解:

在这里插入图片描述

20.2 编写注解处理器

20.2.1 例一

package com.fei.hanhanhan;

public class PasswordUtils {
    @UseCase(id = 1, description = "aaaaa")
    public void f1() {
    }

    @UseCase(id = 2, description = "bbbbb")
    public void f2() {
    }

    @UseCase(id = 3)  // 有默认值的可以不给
    public void f3() {
    }

}

package com.fei.hanhanhan;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class UseCaseTracker {
    public static void trackUseCases(List<Integer> useCases, Class<?> cl) {
        for (Method method : cl.getDeclaredMethods()) {
            UseCase annotation = method.getAnnotation(UseCase.class);
            if (annotation != null) {
                System.out.println("find: " + annotation.id() + " " + annotation.description());
                useCases.remove(new Integer(annotation.id()));
            }
        }

        for (int i : useCases) {
            System.out.println("Missing : " + i);
        }
    }

    public static void main(String[] args) {
        List<Integer> useCases = new ArrayList<Integer>();
        Collections.addAll(useCases,1,2,3,4,5,6,7);
        trackUseCases(useCases,PasswordUtils.class);
    }
}

20.2.2 例二

package com.fei.hanhanhanh;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)   // 类,接口(包含注解类型),enum
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
    public String name() default "";
}

package com.fei.hanhanhanh;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

package com.fei.hanhanhanh;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    String name() default "";
    Constraints constraints() default @Constraints;
}
package com.fei.hanhanhanh;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

package com.fei.hanhanhanh;

import java.sql.SQLOutput;

@DBTable(name = "MEMBER")
public class Member {
    @SQLString(30) String firstName;
    @SQLString(50) String lastName;
    @SQLInteger Integer age;
    @SQLString(value = 30,constraints = @Constraints(primaryKey = true)) String handle;
    static int memberCount;

    public static void main(String[] args) throws Exception {
        Class.forName("com.fei.hanhanhanh.Member");
    }
}

package com.fei.hanhanhanh;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.security.PublicKey;
import java.util.ArrayList;

public class TableCreator {
    public static void main(String[] args) throws Exception {
        args = new String[] {"com.fei.hanhanhanh.Member"};
        if (args.length < 1) {
            System.out.println("null");
            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 + "没有DBTable注解");
                continue;
            }
            String tableName = dbTable.name();
            if (tableName.length() < 1) {
                tableName = cl.getName().toUpperCase();
            }
            ArrayList<String> columnDefs = new ArrayList<String>();
            for (Field field : cl.getDeclaredFields()) {
                String columnName = null;
                Annotation[] anns = field.getDeclaredAnnotations();
                if (anns.length < 1) {
                    continue;
                }
                if (anns[0] instanceof SQLInteger) {
                    SQLInteger sInt = (SQLInteger) anns[0];
                    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];
                    if (sString.name().length() < 1)
                        columnName = field.getName().toUpperCase();
                    else
                        columnName = sString.name();
                    columnDefs.add(columnName + " VARCHAR(" + sString.value() + ")" + getConstraints(sString.constraints()));
                }

                StringBuilder stringBuilder = new StringBuilder("CREATE TABLE " + tableName + "(");
                for (String columnDef : columnDefs) {
                    stringBuilder.append("\n    "  + columnDef + ",");
                }
                String tableCreate =stringBuilder.substring(0,stringBuilder.length() - 1) + ");";
                System.out.println("Table Creation SQL for " + className + " is :\n" + tableCreate);
            }
        }
    }

    public 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;
    }
}

20.2.3 注解不支持继承

20.3 使用apt处理注解

20.4 将观察者模式用于apt

20.5 基于注解的单元测试

20.5.1 编写JUnit测试

什么是单元测试呢?单元测试就是针对最小的功能单元编写测试代码。Java程序最小的功能单元是方法,因此,对Java程序进行单元测试就是针对单个Java方法的测试。

<dependency>
	<groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.2</version>
     <scope>test</scope>
</dependency>
public class Factorial {
    public static long fact(long n) {
        if (n < 0) {
            throw new IllegalArgumentException();
        }
        long r = 1;
        for (long i = 1; i <= n; i++) {
            r = r * i;
        }
        return r;
    }
}
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;

import com.han.Factorial;
import org.junit.jupiter.api.Test;

public class FactorialTest {

    @Test
    void testFact() {
        int[] a1 = new int[]{1,2,3,4,5};
        int[] a2 = new int[]{1,2,3,4,5};
        assertEquals(1, Factorial.fact(1));  // 测试结果与预期不符,assertEquals()会抛出异常
        assertEquals(0.1, Math.abs(1 - 9 / 10.0), 0.0000001);  // 由于浮点数无法精确地进行比较,指一个误差值
        assertArrayEquals(a1,a2); // 期待结果为数组并与期望数组每个元素的值均相等
    }
}

20.5.2 使用Fixture

在测试的时候,我们经常遇到一个对象需要初始化,测试完可能还需要清理的情况。如果每个@Test方法都写一遍这样的重复代码,显然比较麻烦。

JUnit提供了编写测试前准备、测试后清理的固定代码,我们称之为Fixture。

@BeforeEach,@AfterEach

会在运行每个@Test方法前后自动运行,用于实例变量,各个@Test方法中互不影响。

package com.han;

public class Calculator {
    private long n = 0;

    public long add(long x) {
        n = n + x;
        return n;
    }

    public long sub(long x) {
        n = n - x;
        return n;
    }
}

import com.han.Calculator;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CalculatorTest {
    Calculator calculator;

    @BeforeEach
    public void setUp() {
        this.calculator = new Calculator();
    }

    @AfterEach
    public void tearDowm() {
        this.calculator = null;
    }

    @Test
    void testAdd() {
        assertEquals(100, this.calculator.add(100));
        assertEquals(150, this.calculator.add(50));
        assertEquals(130, this.calculator.add(-20));
    }

    @Test
    void testSub() {
        assertEquals(-100, this.calculator.sub(100));
        assertEquals(-150, this.calculator.sub(50));
        assertEquals(-130, this.calculator.sub(-20));
    }
}

@BeforeAll,@AfterAll

在所有@Test方法运行前后仅运行一次,只能初始化静态变量,各个@Test方法中均是唯一实例,影响各个@Test方法。

import com.han.Calculator;
import org.junit.Before;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CalculatorTest {
    Calculator calculator;
    static String db;

    @BeforeAll
    public static void initDatabase() {
        db = "aaa";
    }

    @AfterAll
    public static void dropDatabase() {
        db = null;
    }
    
    @BeforeEach
    public void setUp() {
        this.calculator = new Calculator();
    }

    @AfterEach
    public void tearDowm() {
        this.calculator = null;
    }

    @Test
    void testAdd() {
        assertEquals(100, this.calculator.add(100));
        assertEquals(150, this.calculator.add(50));
        assertEquals(130, this.calculator.add(-20));
    }

    @Test
    void testSub() {
        assertEquals(-100, this.calculator.sub(100));
        assertEquals(-150, this.calculator.sub(50));
        assertEquals(-130, this.calculator.sub(-20));
    }

    
}

20.5.3 异常测试

JUnit提供assertThrows()来期望捕获一个指定的异常。第二个参数Executable封装了我们要执行的会产生异常的代码。

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;

import com.han.Factorial;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;

public class FactorialTest {

    @Test
    void testNegative() {
        assertThrows(IllegalArgumentException.class, new Executable() {
            @Override
            public void execute() throws Throwable {
                Factorial.fact(-2);
            }
        });
		// Java 8开始引入了函数式编程,所有单方法接口都可以简写如下:
        assertThrows(IllegalArgumentException.class, () -> {
            Factorial.fact(-1);
        });
    }
}

20.5.4 条件测试

在运行测试的时候,有些时候,我们需要排出某些@Test方法,不要让它运行。

package com.han;

public class Config {
    public static String getConfigFile(String filename) {
        String os = System.getProperty("os.name").toLowerCase();
        if (os.contains("win")) {
            return "C:\\" + filename;
        }
        if (os.contains("mac") || os.contains("linux") || os.contains("unix")) {
            return "/usr/local/" + filename;
        }
        throw new UnsupportedOperationException();
    }
}

import com.han.Config;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.condition.EnabledOnOs;

public class ConfigTest {

    @Disabled  // 不运行
    @Test
    void testBug101() {  
        // 这个测试不会运行
    }

    @Test
    @EnabledOnOs(OS.WINDOWS)  // 在windows系统运行
    void testWindows() {
        assertEquals("C:\\test.ini",Config.getConfigFile("test.ini"));
    }

    @Test
    @EnabledOnOs({ OS.LINUX, OS.MAC })
    void testLinuxAndMac() {
        assertEquals("/usr/local/test.cfg", Config.getConfigFile("test.cfg"));
    }

    @Test
    @DisabledOnOs(OS.WINDOWS)  // 不在windows运行
    void testOnNonWindowsOs() {
        // TODO: this test is disabled on windows
    }

    @Test
    @EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")  // 只能在64位操作系统上执行的测试
    void testOnlyOn64bitSystem() {
        // TODO: this test is only run on 64 bit system
    }
}

20.5.5 参数化测试

导入包

<dependency>
    <groupId>org.junit.jupiter</groupId>
	<artifactId>junit-jupiter-params</artifactId>
    <version>5.2.0</version>
    <scope>test</scope>
</dependency>
@ValueSource

单个变量放在数组中

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class Test01 {
    @ParameterizedTest
    @ValueSource(ints = { 0, 1, 5, 100 })
    void testAbs(int x) {
        assertEquals(x, Math.abs(x));
    }
}

@MethodSource

多对参数

public class Test01 {
    @ParameterizedTest
    @MethodSource
    void testCapitalize(String input, String result) {
        assertEquals(result, StringUtils.capitalize(input));
    }

    static List<Arguments> testCapitalize() {
        return new ArrayList<Arguments>(Arrays.asList(
                Arguments.of("abc", "Abc"), //
                Arguments.of("APPLE", "Apple"), //
                Arguments.of("gooD", "Good")
        ));
    }
}
@CsvSource
public class Test01 {
    @ParameterizedTest
    @CsvSource({ "abc, Abc", "APPLE, Apple", "gooD, Good" })
    void testCapitalize(String input, String result) {
        assertEquals(result, StringUtils.capitalize(input));
    }
}
@CsvFileSource

存到文件中

public class Test01 {
    @ParameterizedTest
    @CsvFileSource(resources = { "/test.csv" })
    void testCapitalizeUsingCsvFile(String input, String result) {
        assertEquals(result, StringUtils.capitalize(input));
    }
}

JUnit只在classpath中查找指定的CSV文件,因此,test.csv这个文件要放到resources目录下,内容如下:

apple, Apple
HELLO, Hello
JUnit, Junit
reSource, Resource
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值