首页 > 编程语言 >[Java SE/JDK] Java 注解机制

[Java SE/JDK] Java 注解机制

时间:2024-08-17 15:28:02浏览次数:9  
标签:lang Java JDK java 接口 import 注解 方法 SE

1 概述

1.0 引言

  1. 面向切面编程思想(aop)与注解的结合,是实现复杂系统解藕的最终良药。
  • 软件工程的核心思想、目标追求,6字箴言:高内聚,低耦合
  • Java 注解 是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。
  1. jdk、spring、spring boot等卓越框架的巨佬们已经向世人证明了这一点。

  2. 在此前我一直不解Java的注解应该如何使用。通过此番学习后,基本算是明白了,就看未来看看能不能在项目中进行灵活应用了。

  3. 基于以上,也给大家分享、汇报一下 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

注解定义

  • @FunctionalInterfaceJava 8 中的一个注解,用于标记接口为函数式接口Functional Interface)。

函数式接口是指只有一个抽象方法的接口,它可以隐含地转换为 lambda 表达式的语法形式。

  • 一个函数式接口只有一个抽象方法(不包括继承自java.lang.Object的默认方法)的特定接口。

这个抽象方法可以有任意数量的默认方法、静态方法以及覆盖Object类的方法。
关键在于该接口必须确保只有一个未被实现的抽象方法

  • 在 Java 中,函数式接口是专门为了配合 lambda 表达式和方法引用而设计的接口。
  • 单一抽象方法:函数式接口的核心特征是它只包含一个抽象方法。这意味着除了默认方法、静态方法或继承自 java.lang.Object 的方法之外,它不能有其他的抽象方法。
  • 默认方法:Java 8 引入了接口的默认方法,这些方法提供了默认实现,允许接口随着时间的推移而进化,而不破坏现有的实现。默认方法【不计入】函数式接口的抽象方法数量。
  • 静态方法:静态方法也【不计入】抽象方法的数量。因为它们不需要由实现接口的类来实现。
  • 继承自 java.lang.Object 的方法:java.lang.Object 类中的方法,如 equals()hashCode()toString(),同样【不计入】函数式接口的抽象方法数量。
  • 注解 @FunctionalInterface:虽然这个注解不是必需的,但它提供了一种明确的方式告诉编译器和开发者,这个接口是设计为函数式接口的。如果一个标记了 @FunctionalInterface 的接口包含多于一个的抽象方法,编译器会报错。
  • lambda 表达式:函数式接口允许开发者使用 lambda 表达式来提供接口的实现,这是一种简洁的匿名内部类替代方案。
  • 方法引用:除了 lambda 表达式,函数式接口还支持方法引用,这允许开发者直接引用已有的方法构造器来提供接口的实现。
  • 高阶函数:函数式接口可以作为参数传递给其他方法,或者作为其他方法的返回类型,这使得它们成为实现高阶函数(即操作其他函数的方法)的理想选择。

使用示例 : 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 接口的实现

使用示例 : 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;
    }
}

与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 表达式实现了 PredicateConsumer 接口:

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 表达式的另一种方式。方法引用可以直接指定一个已存在的方法名,而不需要显式地写出方法体。

例如,下面的例子中,我们使用方法引用来实现 SupplierBinaryOperator 接口:

Supplier<String> textSupplier = () -> "Hello";
BinaryOperator<String> concatOperator = (a, b) -> a + b;

String result = textSupplier.get() + " " + concatOperator.apply("World!", "!");
System.out.println(result); // 输出 Hello World!

在上面的例子中,我们定义了两个函数式接口 SupplierBinaryOperator,并将它们分别赋予了不同的方法引用。
最后,我们使用 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 注解类别:自定义注解(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 参考文献

标签:lang,Java,JDK,java,接口,import,注解,方法,SE
From: https://www.cnblogs.com/johnnyzen/p/18364444

相关文章

  • @clickhouse/client-web部署后出现ClickHose query error:crypto.randomUUID is not a
    crypto.randomUUID报错我这里是因为使用使用@clickhouse/client-web组件,在服务器部署后在浏览器访问界面导致的crypto.randomUUIDisnotafunction报错如果你用http://localhost:端口,在服务器浏览器上访问这个部署的页面,发现不会报错,这是因为,你使用localhost访问......
  • java
    8.11(1)学生管理系统菜单搭建代码示例:importjava.util.Scanner;publicclassStudentSystem{publicstaticvoidmain(String[]args){loop:while(true){System.out.println("--------欢迎进入学生管理系统---------");System.out.println("1:添加学生");System.out.p......
  • 使用 AWS EKS 部署 Easysearch
    随着企业对数据搜索和分析需求的增加,高效的搜索引擎解决方案变得越来越重要。Easysearch作为一款强大的企业级搜索引擎,可以帮助企业快速构建高性能、可扩展的数据检索系统。在云计算的背景下,使用容器化技术来部署和管理这些解决方案已经成为主流选择,而AmazonElasticKubernetes......
  • sql server 将数据库表里面的数据,转为insert语句,方便小批量转移数据
    createproc[dbo].[proc_insert](@tablenamevarchar(256))asbeginsetnocountondeclare@sqlstrvarchar(4000)declare@sqlstr1varchar(4000)declare@sqlstr2varchar(4000)select@sqlstr='select''insert'+@tablenameselect@sqlstr1=......
  • locale: Cannot set LC_CTYPE to default locale: No such file or directory locale:
    locale:CannotsetLC_CTYPEtodefaultlocale:Nosuchfileordirectorylocale:CannotsetLC_MESSAGEStodefaultlocale:Nosuchfileordirectorylocale:CannotsetLC_COLLATEtodefaultlocale:Nosuchfileordirectory 一、CannotsetLC_CTYPEtodefaul......
  • Java后端面试题(mq相关)(day9)
    目录为什么用MQ?异步、削峰、解耦1.异步处理2.解耦3.削峰填谷Exchange类型什么是死信队列?如何保证消息的可靠性?RabbitMQ中如何解决消息堆积问题?RabbitMQ中如何保证消息有序性?如何防止消息重复消费?(如何保证消息幂等性)为什么用MQ?异步、削峰、解耦MQ(Message......
  • Elasticsearch(ES)(版本7.x)数据更新后刷新策略RefreshPolicy
    Elasticsearch(ES)(版本7.x)数据更新后刷新策略RefreshPolicy介绍ES数据写入后,默认1s后才会被搜索到(refresh_interval为1);这样可能是考虑到性能问题,毕竟实时IO消耗较多资源造成的问题例如一个索引现在有100个文档,当新增一个文档时,立即查询,显示数量为100,并不为101例如......
  • Cookie和Session的区别
    Cookie:Cookie是保存在客户端的一小块文本串的数据。客户端向服务器发起请求时,服务端会向客户端发送一个Cookie,客户端就把Cookie保存起来。在客户端下次向同一服务器再发起请求时,Cookie被携带发送到服务器。服务端可以根据这个Cookie判断用户的身份和状态。Session:Sessi......
  • Java中的图算法:如何实现高效的最短路径计算
    Java中的图算法:如何实现高效的最短路径计算大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!作为开头。最短路径算法是图论中的一个核心问题,广泛应用于网络路由、地图导航等领域。在Java中实现高效的最短路径计算通常涉及到Dijkstra算法和Floy......
  • 点亮职场之路,从优化简历开始 —— 专业Java简历优化服务,助您脱颖而出
    手机或电脑浏览器就可以打开,面霸宝典【全拼音】.com这里可以优化简历,模拟面试,企业项目源码,最新最全大厂高并发面试题,项目场景题,算法题,底层原理题在Java技术的浩瀚星海中,每一位求职者都是独特的星辰,但如何让自己在众多候选人中熠熠生辉,成为企业争抢的“宝藏”?答案,就藏在......