Java 注解深度解析:从基础到实战应用全攻略

110 阅读13分钟

一、注解的概念

注解是一种可以被添加到 Java 代码中的元数据。无论是类、方法、变量、参数还是包,都能够被注解所修饰。其主要作用在于定义一个类、属性或一些方法,以便程序能够在后续的处理过程中依据这些注解信息进行特定的操作。可以将注解形象地理解为一个说明文件,它清晰地告诉应用程序某个被注解的类或属性是什么,以及应该如何对其进行处理。值得注意的是,注解本身对于它所修饰的代码并没有直接的实质性影响,它更多的是为其他工具或程序在编译、运行时提供引导和参考。

二、注解的使用范围

  1. 为编译器提供信息:注解能够被编译器检测到,从而帮助编译器发现代码中的错误或者抑制一些不必要的警告。例如,在一些特定的编程规范要求下,通过注解可以告知编译器某个方法的使用是否符合规范,若不符合则产生相应的提示或错误信息。
  2. 编译时和部署时的处理:众多软件工具能够读取注解信息,并依据这些信息生成代码、XML 文件等。在一些代码生成框架中,注解可以用来指定生成代码的模板、结构或内容,大大提高了代码的生成效率和灵活性。
  3. 运行时的处理:部分注解在程序运行时能够被检测到。这使得程序在运行过程中可以根据注解的存在与否以及注解中所包含的信息,动态地调整自身的行为。例如,在某些框架中,通过运行时检测注解来确定是否需要对某个方法进行额外的处理,如日志记录、权限验证等。

三、自定义注解的步骤

  1. 定义注解
    • 自定义注解需要使用@interface关键字。例如:
package com.example.demo.config;

public @interface MyAnnotation {
    public String name();
    int age();
    String sex() default "女";
}

注解的访问修饰符必须为public,若不写则默认为public。 注解元素的类型只能是基本数据类型、StringClass、枚举类型、注解类型以及一维数组。 注解元素的名称一般定义为名词,如果注解中只有一个元素,将其名字起为value是一种较好的实践方式。这里的()并非定义方法参数的地方,也不能在括号中定义任何参数,它仅仅只是一个特殊的语法形式。 - default关键字用于指定注解元素的默认值,且该默认值的类型必须与元素定义的类型一致。如果某个注解元素没有默认值,那么在后续使用注解时就必须为该类型元素赋值。 2. 配置注解:在需要使用自定义注解的地方,如类、方法、属性等上面添加注解,并按照要求为注解元素赋值。例如:

package com.example.demo.controller;

import com.example.demo.config.MyAnnotation;

public class UserController {

    @MyAnnotation(name = "张三",age = 18,hobby = {"跑步,打游戏"})
    public String get(){
        return "Hello Annotation";
    }
}
  1. 解析注解:利用 Java 的反射机制来读取注解信息并进行相应的处理。例如:
package com.example.demo.test;

import com.example.demo.config.MyAnnotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

public class Test {
    public static void main(String[] args) {
        try {
            //获取 Class 对象
            Class mylass = Class.forName("com.example.demo.controller.UserController");
            //获得该对象身上配置的所有的注解
            Annotation[] annotations = mylass.getAnnotations();
            System.out.println(annotations.toString());
            //获取里面的一个方法
            Method method = mylass.getMethod("get");
            //判断该元素上是否配置有某个指定的注解
            if(method.isAnnotationPresent(MyAnnotation.class)){
                System.out.println("UserController 类的 get 方法上配置了 MyAnnotation 注解!");
                //获取该元素上指定类型的注解
                MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class);
                System.out.println("name: " + myAnnotation.name() + ", age: " + myAnnotation.age()
                        + ",sex:"+myAnnotation.sex()+", hobby: " + myAnnotation.hobby()[0]);
            }else{
                System.out.println("UserController 类的 get 方法上没有配置 MyAnnotation 注解!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

四、注解的基本语法

  1. 最基本的注解定义:如上述自定义注解MyAnnotation的示例,展示了注解中定义元素的基本方式,包括不同类型元素的定义以及默认值的设置。
  2. 常用的元注解
    • @Retention:用于定义注解的保留策略。
      • @Retention(RetentionPolicy.SOURCE):注解仅存在于源码中,在 class 字节码文件中不会包含该注解信息。这种注解通常用于在代码编写阶段为一些工具提供辅助信息,如代码格式化工具、代码检查工具等,在编译后就不再需要。
      • @Retention(RetentionPolicy.CLASS):这是默认的保留策略,注解会在 class 字节码文件中存在,但在运行时无法通过反射等方式获得。它主要用于在编译时让编译器根据注解进行一些处理,如某些优化操作或生成额外的代码,但在运行时对程序的逻辑没有直接影响。
      • @Retention(RetentionPolicy.RUNTIME):注解会在 class 字节码文件中存在,并且在运行时可以通过反射获取到。这使得程序在运行过程中能够根据注解的内容动态地改变自身的行为,如在一些框架中用于实现动态代理、依赖注入、日志记录等功能。在实际开发中,大多数自定义注解都会采用RetentionPolicy.RUNTIME策略,以便在运行时充分发挥注解的作用。
    • @Target:指定被修饰的 Annotation 可以放置的位置,即被修饰的目标。
      • @Target(ElementType.TYPE):表示该注解可以用于接口、类。
      • @Target(ElementType.FIELD):用于属性。
      • @Target(ElementType.METHOD):用于方法。
      • @Target(ElementType.PARAMETER):针对方法参数。
      • @Target(ElementType.CONSTRUCTOR):适用于构造函数。
      • @Target(ElementType.LOCAL_VARIABLE):可用于局部变量。
      • @Target(ElementType.ANNOTATION_TYPE):修饰注解本身,即元注解。
      • @Target(ElementType.PACKAGE):用于包。可以同时指定多个位置,例如@Target({ElementType.METHOD, ElementType.TYPE}),表示此注解可以在方法和类上面使用。
    • @Inherited:指定被修饰的 Annotation 将具有继承性。该注解只对@Target被定义为ElementType.TYPE的自定义注解起作用。它作用于整个程序运行中(@Retention(RetentionPolicy.RUNTIME)),并且只能修饰注解(@Target({ElementType.ANNOTATION_TYPE}))。当一个父类被带有@Inherited注解修饰的注解标注时,其子类在声明部分也能自动拥有该注解。例如,先定义两个注解@HasInherited(包含@Inherited注解)和@NoInherited(反之),再创建具有继承关系的Father类和Child类,当给Father类添加@HasInherited注解时,Child类也会继承该注解;若添加@NoInherited注解,则Child类不会继承。在SpringBoot@SpringBootApplication注解中就使用了@Inherited注解,使得自定义类继承SpringBoot的启动类时也能具备相关特性。
    • @Documented:指定被修饰的该 Annotation 可以被javadoc工具提取成文档。它本身也是一个元注解,通常与其他注解一起使用,以便在生成项目文档时能够包含注解信息,使文档更加完整和详细。

五、注解的特殊语法

  1. 如果注解没有注解类型元素,那么在使用注解时可省略(),直接写为:@注解名。例如:
@Target(value={ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
}

public class UserController {

    @MyAnnotation
    public String get(){
        return "Hello Annotation";
    }
}
  1. 如果注解只有一个注解类型元素,且命名为value,那么在使用注解时可直接写为:@注解名(注解值)。例如:
@Target(value={ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
    String value();
}

public class UserController {

    @MyAnnotation("hello")
    public String get(){
        return "Hello Annotation";
    }
}
  1. 如果注解中的某个注解类型元素是一个数组类型,在使用时又出现只需要填入一个值的情况,那么在使用注解时可直接写为:@注解名(类型名 = 类型值),和标准的@注解名(类型名 = {类型值})等效!例如:
@Target(value={ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
    String[] arr();
}

public class UserController {

    @MyAnnotation(arr = "hello")
    public String get(){
        return "Hello Annotation";
    }
}
  1. 如果注解的@Target定义为Element.PACKAGE,那么这个注解是配置在package-info.java中的,而不能直接在某个类的package代码上面配置。

六、在项目中使用自定义的注解

  1. 环境搭建
    • 新建一个SpringBoot项目,并导入相关的jar坐标,如spring-boot-starter-webspring-boot-starter-aopmybatis-spring-boot-starter等,这些依赖将为项目提供 Web 开发、面向切面编程以及与数据库交互等功能支持。
    • 配置application.yml文件,设置数据源相关信息,包括数据库类型(如使用阿里巴巴的Druid数据源)、数据库的路径、用户名和密码等,同时配置MyBatismapperLocations以及开启日志打印功能,以便在项目运行过程中能够方便地进行数据库操作和调试。
    • 执行sql脚本,创建项目所需的数据库和表,如示例中的annotation数据库以及systemlog表,用于存储系统日志信息。
  2. 创建日志的 MVC
    • 创建日志类SystemLog,使用Lombok注解(如@Getter@Setter@ToString)简化代码编写,该类用于封装系统日志的相关信息,包括日志主键、标题、模块描述、记录时间、调用方法和错误信息等。
    • 创建Service接口SystemLogService,定义createLog方法用于将系统日志信息插入到数据库中。
    • 创建ServiceImplSystemLogServiceImpl,实现SystemLogService接口,通过@Autowired注入SystemLogDao,在createLog方法中调用SystemLogDao的相应方法来完成数据库插入操作。
    • 创建Dao接口SystemLogDao,使用@Mapper注解标识该接口为MyBatisMapper接口,并定义createLog方法,用于与数据库中的systemlog表进行交互,执行插入日志数据的操作。
    • 创建Mapper文件SystemLogMapper.xml,在其中编写SQL语句,实现将SystemLog对象的数据插入到systemlog表中的功能,如insert into systemLog values(#{id},#{title},#{describe},sysdate(),#{方法},#{错误信息})
  3. 自定义注解
    • 创建@Log注解,使用@Target(ElementType.METHOD)指定该注解只能用于方法上,@Retention(RetentionPolicy.RUNTIME)使其在运行时可被获取,@Documented以便在生成文档时包含该注解信息。注解中定义了title(模块名称,默认值为空字符串)和describe(描述,默认值为空字符串)两个元素,用于记录方法的相关信息,以便在日志记录时使用。
  4. 创建 aop 切面
    • 创建LogAspect类,使用@Aspect注解标识该类为一个切面,@Component将其注册为Spring的组件。在类中通过@Autowired注入SystemLogService,用于在切面中记录日志信息到数据库。
    • 定义@Pointcut("@annotation(com.zys.springboot.annotationdemo.config.Log)"),指定切点为带有@Log注解的方法。
    • 分别创建@AfterReturning(方法正常返回后执行)和@AfterThrowing(方法抛出异常时执行)两个通知方法,在这两个方法中都调用handleLog方法来处理日志记录。handleLog方法中,首先通过MethodSignature获取方法签名,进而获取方法名、注解对象等信息,根据注解对象中的信息设置SystemLog对象的相关属性,如id(使用UUID生成)、method(方法名)、title(注解中的title值)、describe(注解中的describe值),若方法抛出异常,则将异常信息设置到SystemLog对象的error属性中,最后通过logService.createLog将日志信息插入到数据库中。
    • 同时还定义了getAnnotationLog方法,用于获取指定切点上的@Log注解对象,该方法在handleLog方法中被调用,以获取注解信息。
  5. 创建测试接口
    • controller包下创建UserController类,使用@RestController注解标识该类为一个RESTful风格的控制器。在类中的方法上使用@Log注解,并为titledescribe元素赋值,如@Log(title = "用户模块",describe = "获取用户列表"),用于记录方法的相关信息,以便在调用这些方法时能够自动记录日志信息到数据库中。

七、结合 SpEL 实现注解动态参数传值

在某些情况下,我们希望自定义注解的参数能够根据运行时的情况动态地传递值,这时可以结合 SpEL(Spring Expression Language)表达式来实现。

  1. 自定义注解:创建@ResourceAccessPermission注解,使用@Inherited@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)注解进行修饰,注解中定义了一个value元素,用于接收用户编号,并且该元素支持 SpEL 表达式。
  2. 创建 SpEL 表达式解析器:创建ExpressionEvaluator类,该类继承自CachedExpressionEvaluator,用于解析 SpEL 表达式。在类中定义了paramNameDiscoverer用于获取参数名,conditionCache用于缓存表达式,targetMethodCache用于缓存目标方法。通过createEvaluationContext方法创建表达式上下文,getTargetMethod方法获取目标方法,getValueByConditionExpression方法根据条件表达式获取 SpEL 取值。同时还定义了ExpressionRootObject内部类,用于封装对象和参数数组,作为表达式求值的根对象。
  3. 配置切面,进行权限认证:创建ResourceAccessPermissionAspect类,使用@Component@Aspect注解将其注册为Spring组件和切面。在类中定义@Pointcut("@annotation(com.zxh.test.annotation.ResourceAccessPermission)"),指定切点为带有@ResourceAccessPermission注解的方法。在@Before通知方法doPermission中,首先获取方法签名、方法对象和参数数组,然后获取@ResourceAccessPermission注解对象,读取注解中的value参数(如果以#开头则按照 EL 处理,否则按照普通字符串处理)。通过ExpressionEvaluator解析 SpEL 表达式获取实际的用户编号值,最后根据用户编号进行权限验证(示例中简单地假设用户编号不等于3时显示无权限,实际应用中需要根据业务需求查询用户与权限分配表进行验证),若权限验证不通过,则抛出RuntimeException异常,在全局异常处理中可以捕获该异常并返回相应的提示信息。
  4. 在接口中使用注解:在接口方法上使用@ResourceAccessPermission注解,并将查询条件中的用户编号作为参数传入注解,如@ResourceAccessPermission("#queryBody.userNo"),这样在方法调用时,切面会根据传入的用户编号进行权限验证,确保系统的安全性与数据的保密性。这种动态参数传值的方式极大地增强了注解的灵活性与实用性,使得我们能够根据不同的业务场景与用户需求,精准地控制程序的执行流程与资源访问权限。例如在一个多用户的文档管理系统中,不同用户对不同文档具有差异化的访问权限,通过此方式可以方便地在方法调用时根据当前用户的编号来确定其是否有权限访问特定文档,有效防止了未经授权的访问与数据泄露风险,提升了整个系统的安全性与可靠性。同时,这种基于注解与 SpEL 表达式相结合的设计模式也为开发人员提供了一种简洁而高效的编程思路,可广泛应用于各类需要动态权限控制或参数传递的业务场景中,进一步拓展了 Java 注解在实际项目开发中的应用深度与广度,助力开发人员构建更加智能、安全且灵活的应用程序架构。