[Java SE/JDK] Java 注解机制[JDK5-]
1 概述
1.0 引言
- 面向切面编程思想(aop)与注解的结合,是实现复杂系统解藕的最终良药。
- 软件工程的核心思想、目标追求,6字箴言:高内聚,低耦合。
- Java 注解 是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。
-
jdk、spring、spring boot、hibernate validation、lombok等卓越框架的巨佬们已经向世人证明了这一点。
-
在此前我一直不解Java的注解应该如何使用。通过此番学习后,基本算是明白了,就看未来看看能不能在项目中进行灵活应用了。
-
基于以上,也给大家分享、汇报一下 Java 注解的学习心得。(当然,有相当部分内容,参考各位前辈们,已注明在参考文献章节。如仍有侵权,请私联。)
1.1 定义
- 在java编程中,注解(
Annotation)是一种元数据,它提供了关于程序代码的额外信息。注解不直接影响程序的执行,但可以【在运行时】提供有关程序的信息,或让【编译器】执行额外的检查。 - Java注解又称Java标注,是在
JDK5时引入的新特性,注解(也被称为元数据(metadata))。 - Java注解提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。
1.2 注解类别:内置注解(Built-in Annotations)
- 这些注解是Java标准库(
java.lang包 或 相关包)中预先定义的、用于特定的编程目的。
例如:
- @Override :表示方法重写父类的方法。
- @Deprecated :标记过时的方法或类,编译器会发出警告。
- @SuppressWarnings :抑制特定的编译器警告。
- @FunctionalInterface :标识一个函数式接口,即只有一个抽象方法的接口。
- @SafeVarargs :表示方法的安全可变参数列表,避免泛型警告。
java.lang.Override : 方法重写
- 一般用在子类覆写父类的方法上。
import java.lang.Override;
/**
* override注解
* @author hulei
* @date 2024/5/10 13:52
*/
public class OverrideAnnotation {
static class BaseUser implements UserInterface {
@Override
public void method() {
new BaseUser().method();
}
}
interface UserInterface {
void method();
}
@Override
public String toString(){
return "Hello World";
}
}
BaseUser类实现了UserInterface接口,并重写了method()方法,方法上标识了@Override注解。
-
不一定非要是实现一个接口,继承一个普通类或者抽象类,重写父类的方法也可。
-
在Java中,如果子类重写父类的方法,但不使用
@Override注解,需要注意:
- 编译器提示:
如果你没有使用@Override,但实际上是重写了父类方法,某些IDE(如 Eclipse, IntelliJ IDEA )会在方法上显示警告,提示你可能遗漏了@Override 注解。虽然这不是强制性的,但添加它有助于提高代码的可读性和清晰度。
- 编译错误:
如果方法签名(包括方法名、参数列表和返回类型)与父类方法不完全匹配,编译器不会报错,因为你实际上并没有重写方法。
这可能导致意外的行为,因为你可能以为你在调用子类的方法,但实际上调用了父类的方法。
- 方法覆盖的确认:
使用@Override可以确保编译器在编译时检查你是否真正重写了父类的方法。如果签名不匹配,编译器会报错,防止因意外的非重写而导致的问题。
- 代码可读性:
添加@Override注解使代码更易读,因为它清楚地表明该方法是用于重写父类方法的。
- 未来修改的保护:
如果父类的签名在未来发生变化,而你没有更新子类的方法签名,没有@Override的子类方法将不再重写父类方法。
而如果有@Override,编译器会主动报错,以提醒你需要更新子类的方法。
- 综上,尽管不是必须的,但推荐在重写父类方法时使用
@Override注解,以确保代码的正确性和一致性。
java.lang.Deprecated : 被弃用的标记
public class DeprecatedAnnotation {
public static void main(String[] args) {
DeprecatedAnnotation deprecatedAnnotation = new DeprecatedAnnotation();
deprecatedAnnotation.method();
}
@Deprecated
public void method() {
System.out.println("DeprecatedAnnotation.method");
}
}
- 调用一个过时的方法,大部分编译器比如IntelliJ IDEA 会给出警告信息,不推荐使用。
像我们在开发过程中使用很多的第三方库或者框架包括jdk自身的大量类库时,可能早期提供的方法或函数有缺陷,但是又被大量的开发者使用,所以不能删除。
这些第三方库的作者就在过时的方法加上这个注解,api调用者在调用这个过时方法就会收到提示,从而查看源码,根据作者的注释指引调用新的更加安全的方法。
java.lang.SuppressWarnings : 抑制编译器特定的告警
import java.util.ArrayList;
import java.util.List;
import java.lang.SuppressWarnings;
public class SuppressWarningsAnnotation {
@SuppressWarnings("all")
public static void addItems(String item){
List items = new ArrayList();
items.add(item);
}
public static void main(String[] args) {
addItems("item");
}
}
- 如果不加
@SuppressWarnings注解,则:如果代码片段中存在不规范的代码时,编译器就会发出告警提示。
看着很不舒服,都是一些无关紧要的提示,比如类型检查操作的警告,装箱、拆箱操作时候的警告等等。
加了@SuppressWarnings("all")这个注解,告警信息就没有了,抑制类所有类型的告警信息,清清爽爽,这对强迫症患者极为友好。
- 一般常用的:
- `@SuppressWarnings("unchecked") :抑制单类型的警告
@SuppressWarnings(value={"unchecked", "rawtypes"}):抑制多类型的警告@SuppressWarnings("all"):抑制所有类型的警告
- 抑制警告的关键字对照表
| 关键字 | 用途 | 描述 |
|---|---|---|
| all | to suppress all warnings | 抑制所有警告 |
| boxing | to suppress warnings relative to boxing/unboxing operations | 抑制装箱、拆箱操作时候的警告 |
| cast | to suppress warnings relative to cast operations | 抑制映射相关的警告 |
| dep-ann | to suppress warnings relative to deprecated annotation | 抑制启用注释的警告 |
| deprecation | to suppress warnings relative to deprecation | 抑制过期方法警告 |
| fallthrough | to suppress warnings relative to missing breaks in switch statements | 抑制确在switch中缺失breaks的警告 |
| finally | to suppress warnings relative to finally block that don’t return | 抑制finally模块没有返回的警告 |
| hiding | to suppress warnings relative to locals that hide variable | 抑制相对于隐藏变量的局部的警告 |
| incomplete-switch | to suppress warnings relative to missing entries in a switch statement (enum case) | 忽略没有完整的switch语句 |
| nls | to suppress warnings relative to non-nls string literals | 忽略非nls格式的字符 |
| null | to suppress warnings relative to null analysis | 忽略对null的操作 |
| rawtypes | to suppress warnings relative to un-specific types when using generics on class params | 使用generics时忽略没有指定相应的类型 |
| restriction | to suppress warnings relative to usage of discouraged or forbidden references | 抑制禁止引用的使用相关的警告 |
| serial | to suppress warnings relative to missing serialVersionUID field for a serializable class | 忽略在serializable类中没有声明serialVersionUID变量 |
| static-access | to suppress warnings relative to incorrect static access | 抑制不正确的静态访问方式警告 |
| synthetic-access | to suppress warnings relative to unoptimized access from inner classes | 抑制子类没有按最优方法访问内部类的警告 |
| unchecked | to suppress warnings relative to unchecked operations | 抑制没有进行类型检查操作的警告 |
| unqualified-field-access | to suppress warnings relative to field access unqualified | 抑制没有权限访问的域的警告 |
| unused | to suppress warnings relative to unused code | 抑制没被使用过的代码的警告 |
java.lang.FunctionalInterface : 函数式接口(JDK8+)
注解定义
@FunctionalInterface是Java 8中的一个注解,用于标记接口为函数式接口(Functional Interface)。
函数式接口是指
只有一个**抽象方法**的接口,它可以隐含地转换为lambda表达式的语法形式。
- 一个函数式接口是只有一个抽象方法(不包括继承自
java.lang.Object的默认方法)的特定接口。
这个抽象方法可以有任意数量的默认方法、静态方法以及覆盖Object类的方法。
关键在于该接口必须确保只有一个未被实现的抽象方法。
@FunctionalInterface注解的作用:
- 编译时检查:
@FunctionalInterface注解会在编译时检查标注的接口是否符合函数式接口的定义,即是否只有一个抽象方法。如果不符合,编译器会报错,提醒开发者修正。- Lambda 表达式支持:函数式接口的存在主要是为了支持 Lambda 表达式,通过 Lambda 表达式可以简化代码,提高代码的可读性。
-
在 Java 中,函数式接口是专门为了配合
lambda表达式和方法引用而设计的接口。 -
事实上,即使没有加
@FunctionalInterface注解,只要符合函数式接口的定义就是函数式接口。
在 Spring Boot 框架中,经常使用的
CommandLineRunner、ApplicationRunner等等都是函数式接口。
特点
-
单一抽象方法:函数式接口的核心特征是它只包含一个抽象方法。这意味着除了默认方法、静态方法或继承自 java.lang.Object 的方法之外,它不能有其他的抽象方法。
-
默认方法:Java 8 引入了接口的默认方法,这些方法提供了默认实现,允许接口随着时间的推移而进化,而不破坏现有的实现。默认方法【不计入】函数式接口的抽象方法数量。
-
静态方法:静态方法也【不计入】抽象方法的数量。因为它们不需要由实现接口的类来实现。
-
继承自
java.lang.Object的方法:java.lang.Object类中的方法,如equals()、hashCode()和toString(),同样【不计入】函数式接口的抽象方法数量。 -
注解
@FunctionalInterface:虽然这个注解不是必需的,但它提供了一种明确的方式告诉编译器和开发者,这个接口是设计为函数式接口的。如果一个标记了@FunctionalInterface的接口包含多于一个的抽象方法,编译器会报错。 -
lambda表达式:函数式接口允许开发者使用lambda表达式来提供接口的实现,这是一种简洁的匿名内部类替代方案。 -
方法引用:除了 lambda 表达式,函数式接口还支持
方法引用,这允许开发者直接引用已有的方法或构造器来提供接口的实现。 -
高阶函数:函数式接口可以作为参数传递给其他方法,或者作为其他方法的返回类型,这使得它们成为实现高阶函数(即操作其他函数的方法)的理想选择。
函数式接口的使用场景
回调函数(Callback Functions)
- 在一些异步操作或模板方法中,我们可以使用函数式接口来传递回调函数,从而实现定制化的操作。
事件监听器(Event Listeners)
- 通过函数式接口可以定义事件监听器,用于监听特定事件的发生并执行相应的处理逻辑。
- 在Spring boot 框架中,事件监听器通常用于处理应用程序内部的事件,如应用启动、关闭、Bean初始化完成等。
Stream API
-
在Java 8及以上版本中,引入了Stream API,可以通过函数式接口来操作集合数据。
-
在Spring应用中,同样可以利用Stream API对集合数据进行处理、过滤、转换等操作,使得代码更为简洁和可读。
以上就是函数式接口的整理,函数式接口是指只包含一个抽象方法的接口。
使用示例 : MyAction
@FunctionalInterface
interface MyAction {
void perform();
}
public class FunctionalInterfaceAnnotation2 {
public static void callAction(MyAction action) {
action.perform();
}
public static void main(String[] args) {
//使用方式1 (推荐)
//callAction(() -> System.out.println("MyAction performed using lambda expression!"));
//使用方式2
MyAction action = () -> { System.out.println("MyAction performed using lambda expression!"); };
FunctionalInterfaceAnnotation2.callAction(action);
}
}
在这个例子中,
MyAction是一个函数式接口,它只包含一个抽象方法perform()。
main方法中的callAction方法接受一个Action接口的实现作为参数,并调用它的perform()方法。
通过lambda表达式() -> System.out.println("Action performed using lambda expression!"),我们简洁地提供了MyAction接口的实现
案例使用示例 : Calculator
Calculator
@FunctionalInterface
public interface Calculator {
int calculate(int a, int b);
}
FunctionalInterfaceTest
public class FunctionalInterfaceTest {
public static void main(String[] args) {
// 使用 lambda 表达式实现加法
Calculator addition = (a, b) -> a + b;
System.out.println("3 + 5 = " + addition.calculate(3, 5)); // 输出 8
// 使用 lambda 表达式实现减法
Calculator subtraction = (a, b) -> a - b;
System.out.println("7 - 2 = " + subtraction.calculate(7, 2)); // 输出 5
// 使用 lambda 表达式实现乘法
Calculator multiplication = (a, b) -> a * b;
System.out.println("4 * 6 = " + multiplication.calculate(4, 6)); // 输出 24
}
}
out
3 + 5 = 8
7 - 2 = 5
4 * 6 = 24
- 案例分析
Calculator 是一个函数式接口,只包含一个抽象方法 calculate
通过 lambda 表达式分别实现了加法、减法和乘法,并在 main 方法中进行了调用
使用示例 : 使用java.util.function包中的函数式接口(Predicate/Function/Consumer)
- 结合使用
Predicate、Function和Consumer等接口来进行数据处理和过滤
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
public class FunctionalInterfaceTest {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Doe", "Jane", "Smith", "Alice");
// 使用 Predicate 过滤出长度大于等于4的字符串
Predicate<String> filterPredicate = str -> str.length() >= 4;
// 使用 Function 将字符串转换为大写
Function<String, String> toUpperCaseFunction = String::toUpperCase;
// 使用 Consumer 输出字符串
Consumer<String> printConsumer = System.out::println;
// 结合 Predicate、Function 和 Consumer 进行数据处理和输出
names.stream().filter(filterPredicate).map(toUpperCaseFunction).forEach(printConsumer);
}
}
out
JOHN
JANE
SMITH
ALICE
- 案例分析
使用了 Predicate 进行字符串长度的过滤,然后使用 Function 将过滤后的字符串转换为大写,最后使用 Consumer 输出结果
使用示例 : java.util.function.Function
- 拿JDK官方的Function函数式接口为例
打上@FunctionalInterface注解的接口,就可以使用java8提供的lamda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的)。
import java.lang.FunctionalInterface;
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
使用示例 : 优雅的Java应用程序的启停钩子框架
- 推荐文献
与Lambda表达式关联
- 函数式接口与 Lambda 表达式在 Java 8 中的结合使用是现代 Java 编程的一个重要特性。
Lambda表达式是一种简洁的匿名函数语法,允许开发者以简洁的方式定义行为(代码块)。
由于Lambda表达式本身不包含类型信息,Java编译器需要一种机制来确定Lambda表达式对应的目标类型。
函数式接口就扮演了这一角色——Lambda表达式可以被赋值给任何兼容的函数式接口类型,编译器会依据接口的唯一抽象方法来推断Lambda表达式的参数类型和返回类型。
- 类型推断:函数式接口允许编译器通过接口中唯一的抽象方法来推断 Lambda 表达式的参数和返回值类型。这意味着开发者在编写 Lambda 表达式时不必显式地声明参数类型和返回类型。
- 简洁性:Lambda 表达式提供了一种更简洁的方式来实现函数式接口的抽象方法,从而减少了模板代码,使代码更加简洁和易于理解。
- 匿名性:Lambda 表达式本质上是一种匿名函数,它们没有名称,并且可以在需要函数式接口类型的地方直接使用。
- 兼容性:Lambda 表达式可以赋值给任何兼容的函数式接口类型,这意味着只要函数式接口中有一个与 Lambda 表达式兼容的抽象方法,就可以使用 Lambda 表达式来实现它。
- 参数推断:如果函数式接口的抽象方法有参数,Lambda 表达式中的参数列表将与这些参数一一对应。如果 Lambda 表达式不需要参数,它可以不带参数列表。
- 方法体:Lambda 表达式的主体可以是一个表达式(可以返回一个值)或者是一个代码块(可以包含多条语句,但必须包含一个 return 语句来提供返回值,除非返回类型为 void)。
- 上下文推断:在某些情况下,即使没有函数式接口,Lambda 表达式也可以通过上下文推断来确定其类型,这通常发生在使用 Lambda 表达式作为方法参数时,例如在使用 java.util.stream API 时。
以
MyAction的例子为例,MyAction是一个函数式接口,它有一个抽象方法perform()。
在main方法中,我们通过Lambda表达式() -> System.out.println("...")来提供这个接口的实现,并将这个Lambda表达式作为参数传递给callAction方法。
编译器能够根据函数式接口中的抽象方法推断Lambda表达式的类型。
类型检查与编译错误
-
当一个接口被标注为
@FunctionalInterface后,编译器会对该接口进行严格的检查。如果该接口不符合函数式接口的定义(即:存在多个抽象方法),编译器会抛出错误。这为开发者提供了明确的编译时保障,确保所标记的接口确实符合函数式接口的要求。 -
编译时检查:当一个接口被标记为
@FunctionalInterface时,编译器会检查该接口是否只有一个抽象方法。如果有多个抽象方法,编译器会报错,防止开发者无意中创建了一个不符合函数式接口定义的接口。
错误示例
@FunctionalInterface
interface InvalidFunctionalInterface {
void performAction(); // 抽象方法
void anotherAction(); // 第2个抽象方法,会导致IDE报编译错误: Multiple non-overriding abstract methods found in interface org.example.annotation.InvalidFunctionalInterface
}
-
防止错误:这个注解帮助开发者避免错误,因为它强制要求接口设计者明确接口的用途,即作为一个函数式接口。
-
代码清晰:通过使用
@FunctionalInterface注解,代码的可读性和清晰度得到提高,因为其他开发者可以立即识别出该接口是用于lambda表达式或方法引用的。 -
非必需性:虽然
@FunctionalInterface注解有助于清晰地标识函数式接口,但它并不是技术上必需的。即使没有这个注解,只要接口只有一个抽象方法,它仍然可以被用作函数式接口。 -
默认方法、静态方法:即使接口有多个默认方法或静态方法,只要它只有一个抽象方法,它仍然可以被标记为
@FunctionalInterface。默认方法和静态方法不影响函数式接口的定义。 -
重载:需要注意的是,尽管
Java允许方法重载,但在函数式接口中,所有方法(包括默认方法和静态方法)在编译时都被视为具有不同的签名,因此不会影响接口作为函数式接口的有效性。 -
最佳实践:作为一种最佳实践,即使在技术上不需要
@FunctionalInterface注解的情况下,许多开发者也会使用它来明确接口的意图,从而提高代码的可维护性和可读性。
通过这种方式,
@FunctionalInterface注解成为了 Java 语言中一个有用的工具,帮助开发者编写更清晰、更健壮的代码。
使用场景:Lambda表达式和方法引用的类型
- 函数式接口常用于作为Lambda表达式或方法引用的目标类型。 Lambda表达式是一种简洁的方式来表示一个只有一个抽象方法的接口的实现。Lambda表达式可以用更少的代码来替代传统的匿名内部类,使得代码更加简洁易读。
例如,下面的例子中,我们使用 Lambda 表达式实现了
Predicate和Consumer接口:
List<String> myList = Arrays.asList("apple", "banana", "cherry");
boolean containsApple = myList.stream().anyMatch(s -> s.equals("apple"));
System.out.println(containsApple); // 输出 true
myList.forEach(System.out::println);
在上面的例子中,我们首先创建了一个字符串列表 myList,然后使用 anyMatch 方法查找其中是否存在包含 “apple”元素。接着,我们使用 forEach 方法遍历每个元素并打印出来。
- 方法引用是用来简化 Lambda 表达式的另一种方式。方法引用可以直接指定一个已存在的方法名,而不需要显式地写出方法体。
例如,下面的例子中,我们使用方法引用来实现
Supplier和BinaryOperator接口:
Supplier<String> textSupplier = () -> "Hello";
BinaryOperator<String> concatOperator = (a, b) -> a + b;
String result = textSupplier.get() + " " + concatOperator.apply("World!", "!");
System.out.println(result); // 输出 Hello World!
在上面的例子中,我们定义了两个函数式接口
Supplier和BinaryOperator,并将它们分别赋予了不同的方法引用。
最后,我们使用get()方法获取textSupplier的返回值,并将它与 "World!" 拼接起来,得到了 "Hello World!" 的结果。
Lambda 表达式和方法引用都可以作为函数式接口的目标类型,使得代码更加简洁易懂,同时也提高了代码的可重用性。
使用场景:高阶函数参数和返回值
- 函数式接口广泛用作高阶函数(即接受函数作为参数或返回函数的函数)的参数类型或返回类型。高阶函数是函数式编程的一个重要概念,它允许函数接受其他函数作为参数,或者返回函数作为结果。在Java中,函数式接口是实现高阶函数的关键,因为它们可以被用作lambda表达式或方法引用的目标类型。
例如,java.util.concurrent.ExecutorService 的 submit 方法接受一个 Callable 接口作为参数,Callable 是一个函数式接口,它的唯一抽象方法 call 返回一个结果,并可能抛出一个异常。这里是一个使用 ExecutorService 的 submit 方法的简单例子:
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
public class ExecutorServiceExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
// 创建一个Callable任务
Callable<String> task = () -> {
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "任务完成!";
};
// 提交Callable任务到ExecutorService
Future<String> future = executor.submit(task);
// 获取并打印任务的结果
System.out.println(future.get());
// 关闭ExecutorService
executor.shutdown();
}
}
java.lang.SafeVarargs
import java.util.List;
import java.util.Optional;
/**
* 注解:SafeVarargs示例
*/
public class SafeVarargsAnnotations {
@SafeVarargs
static void function(List<String>... stringLists) {
}
abstract static class BaseUser implements UserInterface {
@SafeVarargs
final <T> void gamma(T... ts) {
}
@Override
@SafeVarargs
public final void method(Optional<Object>... optionals) {
UserInterface.super.method(optionals);
}
}
interface UserInterface {
default void method(Optional<Object>... optionals) {
}
@SafeVarargs
static <T> void gamma(Class<T>... classes) {
}
void method();
}
}
- 方法的参数包含可变参数列表时,不加这个
@SafeVarargs注解就会有告警信息,比如上面的代码,method方法有可变参数列表,没有加注解,产生类型安全和泛型相关提示
1.3 注解类别:元注解(Meta-Annotations)
-
元注解是用于注解其他注解的注解,是所有其他注解的基础,它们定义了注解的行为和生命周期。
-
主要包括:
@Retention:定义注解的保留策略,可以是SOURCE(只存在于源码中)、CLASS(编译时丢弃,存在于字节码中但不运行时可用)或RUNTIME(运行时可通过反射访问)。
@Target:指定注解可以应用于哪些程序元素,如类、方法、字段等。
@Documented:指示是否将注解包含在生成的Javadoc中。
@Inherited:允许子类继承父类的注解(仅适用于类,不适用于方法或字段)。
java.lang.annotation.Retention
- 注解定义 :
@Retention
package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Retention {
RetentionPolicy value();
}
- RetentionPolicy
package java.lang.annotation;
public enum RetentionPolicy {
// 注解保留在源代码中,但是编译的时候会被编译器所丢弃。
// 如: @Override , @SuppressWarnings
SOURCE,
// 默认的policy。注解会被保留在class文件中,但在运行时期间就不会识别这个注解
CLASS,
// 注解会被保留在class文件中,同时运行时期间也会被识别。故可使用反射机制获取注解信息。
// 比如 @Deprecated
// 大部分情况下,我们都是需要使用 RUNTIME Policy。
RUNTIME;
private RetentionPolicy() {
}
}
应用案例:@SuppressWarnings
package java.lang;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
@SuppressWarnings注解源码,就只有一个 @Retention注解
打上@Retention注解的其他注解,有三个保留策略,上面已经说明。
应用案例:@MyClassRuntimeAnnotation
-
大部分情况下,我们都是使用RUNTIME这个Policy。 下面就是一个RUNTIME Annotation的例子。
-
定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyClassRuntimeAnnotation {
String name();
int level() default 1;
}
- 引用注解
在CLASS前面使用这个Annotation
import java.lang.annotation.Annotation;
import java.util.Arrays;
@MyClassRuntimeAnnotation(name = "simple", level = 10)
public class SimpleObject {
}
- 基于反射,使用注解
最后写一个testcase通过反射可以获取这个类的Annotation进行后续操作。
public static void main(String[] args) {
Annotation[] annotations = SimpleObject.class.getAnnotations();
System.out.println(Arrays.toString(annotations));
MyClassRuntimeAnnotation myClassAnno = SimpleObject.class.getAnnotation(MyClassRuntimeAnnotation.class);
System.out.println(myClassAnno.name() + ", " + myClassAnno.level());
System.out.println(myClassAnno == annotations[0]);
}
output
[@org.example.annotation.my.MyClassRuntimeAnnotation(level=10, name=simple)]
simple, 10
true
java.lang.annotation.Target
- 注解定义
如果一个注解上有@Target注解,则@Target注解声明了这个注解可以使用的地方
package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Target {
ElementType[] value();
}
- ElementType
package java.lang.annotation;
public enum ElementType {
TYPE,
FIELD,
METHOD,
PARAMETER,
CONSTRUCTOR,
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE;
private ElementType() {
}
}
应用案例:@MyTargetAnnotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//声明一个注解
//@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ...})
@Target(ElementType.METHOD)//指定本注解可以应用于 Method/方法
@Retention(RetentionPolicy.RUNTIME)//保留到运行时,可以通过反射访问
public @interface MyTargetAnnotation {
String value() default "";//定义一个名为 value 的成员,其默认值为 ""}
}
比如这个自定义注解,就只能在方法上使用,ElementType.METHOD枚举就是方法声明限制,关于ElementType枚举,可以自行查看里面的枚举信息
当然,后面可以写多个使用场景的枚举声明
小结
- 还有的注解,没有加@Target注解,比如上面的@SuppressWarnings注解。一个注解上没有加使用范围的注解@Target,那这个注解可以使用在任何能够使用注解的地方。
所以 @SuppressWarnings 不包含自己的 @Target 注解,意味着它理论上可以应用于 Java 规范中任何允许注解的地方。
然而,它实际上的使用受到限制,尤其是不能在表达式上下文中使用,这是因为其设计目的和 Java 语言规范的限制。
-
设计目的:@SuppressWarnings 的设计初衷是为了告诉编译器在特定的范围(如类、方法、字段等)内忽略特定类型的警告。它是为了简化开发过程,允许开发者在明知某些代码可能引起编译器警告,但确认这些警告不影响程序正确性的情况下,有选择地忽略这些警告。因此,它主要应用于编译单位的较大结构上。
-
表达式上下文限制:表达式上下文通常涉及更细粒度的操作,如赋值、方法调用、算术运算等。在这些上下文中使用 @SuppressWarnings 不符合其设计逻辑,因为这些地方通常不涉及整体性的类型或结构警告,而是更具体的、即时的操作。如果允许在表达式中使用,不仅会增加语言的复杂性,还可能引发滥用,使得代码难以理解和维护。
-
类型注解与普通注解的区别:类型注解(自 Java 8 引入)专门设计用于标注类型声明,包括泛型类型参数、返回类型、参数类型等,而 @SuppressWarnings 并不属于这一类别。类型注解可以在某种程度上改变编译器对类型的理解,而 @SuppressWarnings 仅用于指示编译器如何处理警告信息,不改变代码的类型系统或结构。
-
Java 语言规范限制:即使 @SuppressWarnings 没有限定其 @Target,Java 语言规范和编译器实现也决定了哪些注解可以用在哪些上下文中。表达式上下文通常不接受注解,特别是像 @SuppressWarnings 这样旨在影响编译器警告处理的注解,因为这不符合语言的语义和设计哲学。
综上所述,@SuppressWarnings 不能在表达式上下文中使用,主要是由于其设计意图、语言规范的限制以及为了保持语言的清晰度和简洁性。
java.lang.annotation.Documented
这个注解不重要,表示是否将注解包含在生成的Javadoc中。加不加完全在于我们自己,只要知道的用途就行了
java.lang.annotation.Inherited
- 这个注解还是比较重要的,允许子类继承父类的注解(仅适用于类,不适用于方法或字段)。
- 什么意思呢? 一个注解上有
@Inherited注解,那么:当我们把这个注解打在一个类上时,如果这个类有子类,那么这个子类继承父类的这个注解
package com.datastructures;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* @author hulei
* @date 2024/5/10 17:01
*/
public class InheritedAnnotation {
/**
* 自定义注解
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface InnerAnnotation{
String value() default "";
}
/**
* 父类,使用了自定义注解
*/
@InnerAnnotation(value = "父类注解")
public static class ParentClass {
}
/**
* 子类继承父类
*/
public static class ChildClass extends ParentClass {
}
public static void main(String[] args) {
Class<?> childClass = ChildClass.class;
if (childClass.isAnnotationPresent(InnerAnnotation.class)) {
InnerAnnotation annotation = childClass.getAnnotation(InnerAnnotation.class);
System.out.println("Value from InnerAnnotation: " + annotation.value());
} else {
System.out.println("No InnerAnnotation found.");
}
}
}
output
Value from InnerAnnotation: 父类注解
运行可以看到,子类也获取到了这个注解
那我们改造下,把注解上的@Inherited注解去掉,再执行看看
No InnerAnnotation found.
可以看到子类没有获取到父类的注解了,即没有从父类继承。
1.4 注解类别: 第三方库注解(数据校验框架/lombok/spring/...)
javax/jakarta.validation.constraints.* :数据校验框架(jakarta.validation:jakarta.validation-api/javax.validation:validation-api/org.hibernate.validator:hibernate-validation/spring-boot-starter-validation)
- 推荐文献
lombok.SneakyThrows
- 产生背景
在 Java 开发中,异常处理是一个不可避免的重要部分。我们经常需要处理各种检查型异常(
checked exceptions),这有时会导致代码变得冗长且难以维护。为了简化异常处理,Lombok 提供了一个强大的注解——@SneakyThrows。
- 源码定义
package lombok;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.SOURCE)
public @interface SneakyThrows {
Class<? extends Throwable>[] value() default {Throwable.class};
}
@SneakyThrows注解 简介
@SneakyThrows是 Lombok 提供的一个注解,旨在帮助开发者简化异常处理。
它允许方法抛出检查型异常、而无需显式声明或捕获这些异常。
这对于那些不希望在方法签名中声明异常或不愿意编写复杂的try-catch块的场景非常有用。
- lombok 简介
在深入探讨
@SneakyThrows之前,先简单介绍一下 Lombok。
Lombok 是一个 Java 库,它通过注解处理器(Annotation Processor)在编译时自动生成代码,从而减少样板代码(boilerplate code),使代码更加简洁和易于维护。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- 1.18.22 -->
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
- @SneakyThrows 的使用
使用
@SneakyThrows非常简单。你只需将其添加到需要简化异常处理的方法上即可。
例如,以下代码展示了如何使用@SneakyThrows读取文件内容,而不显式处理可能抛出的IOException。
import lombok.SneakyThrows;
import java.nio.file.Files;
import java.nio.file.Paths;
public class FileReader {
@SneakyThrows
public void readFile(String path) {
// 此处可能抛出 IOException,但我们不需要显式处理它
Files.readAllLines(Paths.get(path));
}
}
在这个例子中,
readFile方法尝试读取文件内容。
如果不使用@SneakyThrows,我们通常需要在方法签名中声明throws IOException,或者在方法内部使用try-catch块来捕获和处理异常。
而使用@SneakyThrows后,这些都不再需要。
- @SneakyThrows 的工作原理
当你在方法上使用
@SneakyThrows注解时,Lombok 会在编译时生成一个try-catch块,捕获所有可能抛出的检查型异常,并将它们转换为RuntimeException或其子类,从而避免方法签名中出现throws声明。
这种做法简化了代码,但也带来了一些潜在的风险。
- @SneakyThrows 的风险和注意事项
尽管
@SneakyThrows可以简化异常处理,但它也带来了一些值得注意的风险:
- 异常处理的不明确性
使用@SneakyThrows后,方法不再显式声明可能抛出的检查型异常。这会使得调用者在使用该方法时,不清楚具体可能抛出的异常类型,进而影响异常处理的逻辑和代码的可读性。
- 调试困难
由于@SneakyThrows将检查型异常转换为运行时异常,调试过程中可能难以追踪异常的来源和具体类型。这会增加定位问题和解决问题的难度,尤其是在复杂系统中。
- 掩盖异常处理问题
@SneakyThrows可能掩盖一些本应显式处理的异常情况。这样做可能导致在程序运行时发生未处理的异常,进而引发潜在的运行时错误。
- 团队协作和代码可维护性
如果团队中的其他成员不熟悉 Lombok 或@SneakyThrows,他们可能对异常处理的逻辑感到困惑。这不仅会影响代码的可读性,还可能导致维护困难。
- 异常的处理和恢复
将检查型异常转换为运行时异常后,方法的调用者不再需要显式处理这些异常。然而,在某些情况下,你可能需要对异常做更细致的处理(如日志记录或恢复操作),而@SneakyThrows会忽略这些需求。
- 使用 @SneakyThrows 的建议
鉴于@SneakyThrows的潜在风险,以下是一些使用建议:
- 适度使用
@SneakyThrows适合那些异常处理逻辑简单且明确的场景。
对于复杂的业务逻辑,尤其是涉及到资源管理或需要详细异常处理的地方,建议避免使用该注解,以免影响代码的可维护性。
- 明确文档
在使用@SneakyThrows的地方,添加详细的注释和文档,说明为什么使用该注解,以及可能抛出的异常类型。这可以帮助团队成员更好地理解代码。
- 团队协作
确保团队中的每个成员都理解@SneakyThrows的作用和使用场景,并在代码审查过程中注意它的使用情况,保持代码风格的一致性和清晰性。
- 测试覆盖
在使用@SneakyThrows的方法上,进行充分的单元测试和集成测试,以确保方法在运行时不会出现未预料的异常。
- 参考文献
1.X 注解类别:自定义注解(Custom Annotations)
- 开发者可以使用 @interface 关键字创建自己的注解,根据需求定义注解的行为和用途。自定义注解可以结合元注解来定义其行为。
例如通过@Retention和@Target来控制自定义注解的生命周期和应用范围。
案例:用于记录日志的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface LogExecution {
String message() default "";
}
这个注解可以应用于方法,表示在执行该方法前/后需要记录日志。
- 切面类aop
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Around("@annotation(LogExecution)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getSignature().getDeclaringTypeName();
String message = joinPoint.getSignature().getAnnotation(LogExecution.class).message();
long start = System.currentTimeMillis();
logger.info("Starting method: {}.{} with message: {}", className, methodName, message);
Object result = joinPoint.proceed(); // 继续执行目标方法
long elapsedTime = System.currentTimeMillis() - start;
logger.info("Completed method: {}.{} in {}ms", className, methodName, elapsedTime);
return result;
}
}
- 实际代码调用
@Service
public class SomeService {
@LogExecution(message = "Executing business logic")
public String performTask() {
// 示例业务逻辑
try {
Thread.sleep(100); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
return "Task completed";
}
}
后面的注解,笔者就不再提供aop相关代码了,和日志注解类似,比较简单
案例:用于数据验证的注解
比如,邮箱格式校验
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Constraint(validatedBy = EmailValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailVaild{
String message() default "邮箱格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
案例:事务管理的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Transactional {
boolean readOnly() default false;
}
这个注解用于标记一个方法需要在数据库事务中执行,readOnly 参数表示是否为只读事务。
案例:权限校验的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresRole {
String[] roles() default {};
}
这个注解用于标记一个方法或类需要特定的角色才能访问,roles 参数是角色的数组。
案例:缓存结果的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CacheResult {
long cacheTime() default 60; // 缓存60秒
}
这个注解用于标记一个方法的结果应该被缓存一定时间,cacheTime 参数表示缓存的秒数。
小结:注解与AOP切面编程
在实际使用中,这些注解通常会与AOP(面向切面编程)框架结合,如Spring AOP,以便在运行时动态地处理注解的逻辑。
2 FAQ
Q: 注解中的属性省略问题
这一节是笔者在学习时遇到的疑问,这里作为记录

这个注解有两个属性,value和logical ,@HasPermissions(“system:user:query”)
这种写法会把属性值默认给value,注意必须要有名为value的属性,并且其他属性都有默认值才可以
否则得显示给属性赋值
@HasPermissions(value = {"om:deviceCascade:edit","om:deviceCascade:add"},logical = Logical.OR)
总结:
- 如果注解只有一个属性,那么肯定是赋值给该属性。
- 如果注解有多个属性,而且前提是这多个属性都有默认值,那么你不写注解名赋值,会赋值给名字为“value”这属性或者赋值给多个属性中唯一没有默认值的属性。
- 如果注解有多个属性,其中有没有设置默认值的属性,那么当你不写属性名进行赋值的时候,是会报错的。
X 参考文献
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!

浙公网安备 33010602011771号