文章目录
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