前端视角 Java Web 入门手册 2.10: Java Core ——注解

81 阅读6分钟

TypeScript 装饰器与 Java 注解

在 TypeScript 中如果希望方法在执行之前打印方法名,可以使用装饰器(Decorator)在不侵入函数实现的前提下实现此功能,类似于 Java AOP 的理念

function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Method ${propertyKey} is called.`);
    const result = originalMethod.apply(this, args);
    return result;
  };
  return descriptor;
}

class MyClass {
  @logMethod
  myMethod() {
    // do something
  }
}

const myClass = new MyClass();
myClass.myMethod();

Java 注解(Annotation)和 TypeScript 装饰器都是元编程的理念,允许开发者将额外的信息(元数据)附加到代码中的各个元素,如类、方法、字段、参数等。这些元数据在编译时、类加载时或运行时被读取和处理,用于生成代码、进行配置或执行其他特定操作。

元数据是一种描述数据的数据。在编程中,元数据通常用于描述类、方法、属性和参数等代码结构的信息,例如它们的名称、类型、访问权限、默认值、注释等等。

在 Java 程序设计中,注解有非常广泛的用处

  • 代码生成:自动生成重复性代码,减少手工编码量
  • 配置管理:替代 XML 等配置文件,实现更简洁的配置方式
  • 行为修改:通过框架在运行时根据注解的元数据调整程序行为
  • 编译时检查:通过自定义注解实现更严格的类型检查和代码验证

注解生效阶段

注解可以应用于类、方法、字段、参数等,Java 中注解以 @ 符号开头,后跟注解名称。部分注解可以带有参数,用于传递额外的信息。Java 提供了一些内置注解,主要用于编译器和工具的交互:

  • @Override:标识方法覆盖父类的方法
  • @Deprecated:标识已过时的方法或类
  • @SuppressWarnings:指示编译器忽略特定的警告

根据注解生效阶段不同,可以把注解分为三类

源代码注解

源代码注解只在源码阶段生效,主要是为了提升代码可读性和可维护性,编译器会忽略这些注解,不会将其生成到 class 文件中,@Override、@Deprecated、@SuppressWarnings 都属于此类

import java.util.ArrayList;

public class TestAnnotation<E> extends ArrayList<E> {
  @Override 
  public boolean clear() {
    return true;
  }
}

ArrayList 的 clear 返回值为 void,当我们试图重写 clear 方法,却使用了错误的方法名、返回值类型、参数列表时候,IDE 会提示错误

编译时注解

编译时注解在编译阶段起作用,编译器在生成 class 文件时会根据注解的信息生成相应的代码。这种注解的作用主要是为了生成辅助代码和文件,用于支持程序的运行。为对象自动生成冗长的模板代码让代码变的更加简洁的 lombok 注解就属于编译时注解

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter // 自动生成getter方法
@Setter // 自动生成setter方法
@ToString // 自动生成toString方法
public class Person {
    private String name;
    private int age;
    private String address;
}

代码在编译时会自动生成 getter、setter 和 toString 方法

public class Person {
    private String name;
    private int age;
    private String address;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getAddress() {
        return this.address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "Person(name=" + this.getName() + ", age=" + this.getAge() + ", address=" + this.getAddress() + ")";
    }
}

运行时注解

运行时注解在程序运行时起作用,可以通过反射机制获取并处理,在一些需要动态生成代码的场景中非常有用,Spring 框架中的 @Autowired 注解就是运行时注解

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    public void registerUser(String name) {
        userService.createUser(name);
    }
}

@ Autowired标记在构造函数上,表明 UserService 的依赖应自动注入到 UserController 中

自定义注解

除了使用内置注解,Java 允许开发者根据需求创建自定义注解,以在代码中传递特定的元数据。自定义注解通过 @interface 关键字定义。可以为注解指定元素(类似于方法),以接受参数。

首先定义一个自定义注解 @LogExecutionTime

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 LogExecutionTime {
}

然后可以在业务逻辑类中,标记需要记录其执行时间的方法

public class Calculator {

    @LogExecutionTime
    public int add(int a, int b) throws InterruptedException {
        Thread.sleep(1000); // 模拟耗时操作
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }
}

为了自动拦截被 @LogExecutionTime 标记的方法,可以使用动态代理或 AOP 框架,因为还没有介绍 Spring 框架,先来了解一下利用 Java 反射机制实现动态代理

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class LogProxy {

    // 创建代理对象
    public static <T> T createProxy(T target) {
        return (T) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            new LogInvocationHandler(target)
        );
    }

    // 代理逻辑处理器
    private static class LogInvocationHandler implements InvocationHandler {
        private final Object target;

        public LogInvocationHandler(Object target) {
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Method targetMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());

            // 检查是否有 @LogExecutionTime 注解
            if (targetMethod.isAnnotationPresent(LogExecutionTime.class)) {
                long start = System.currentTimeMillis();
                Object result = targetMethod.invoke(target, args);
                long end = System.currentTimeMillis();
                System.out.println("方法 " + targetMethod.getName() + " 执行耗时: " + (end - start) + "ms");
                return result;
            } else {
                return targetMethod.invoke(target, args);
            }
        }
    }
}

这样可以进行代码测试了

public class Main {
    public static void main(String[] args) throws InterruptedException {
        // 使用动态代理
        Calculator proxyCalculator = LogProxy.createProxy(new Calculator());
        proxyCalculator.add(2, 3);       // 输出耗时
        proxyCalculator.subtract(5, 2);  // 无输出
    }
}

使用注解的好处

在现代 Java 开发中注解被广泛使用,尤其是在框架开发中(如 Spring、Hibernate 等)。注解的使用带来了许多优点

增强代码可读性和可维护性

通过在类、方法或字段上添加注解,开发者可以清晰地表达其意图,而无需依赖复杂的配置文件或注释

import org.springframework.stereotype.Service;

@Service
public class UserService {
    // Service 组件的逻辑
}

减少样板代码

注解可以有效减少重复性和冗长的代码,简化开发过程。例如,在使用 JPA(Java Persistence API)进行对象关系映射时,通过注解可以省略大量的 XML 配置

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class User {
    @Id
    private Long id;
    private String name;
    private String email;

    // Getters and Setters
}

简化配置与集成

注解允许开发者在代码中直接进行配置,避免了外部配置文件的复杂性。在 Spring 框架中,使用注解可以轻松完成依赖注入

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderService {
    
    @Autowired
    private PaymentService paymentService;
    
    public void processOrder() {
        paymentService.processPayment();
    }
}

编译时检查

注解不仅在运行时发挥作用,还可以在编译时提供额外的检查和验证。例如,@Override 注解确保子类方法正确覆盖父类方法,避免因方法签名错误导致的潜在问题

class Parent {
    public void display() {
        System.out.println("Parent Display");
    }
}

class Child extends Parent {
    @Override
    public void display() {
        System.out.println("Child Display");
    }
    
    // 如果方法签名错误,编译器将报错
    // @Override
    // public void disply() {
    //     System.out.println("Child Display");
    // }
}

实现 AOP

注解在面向方面编程中发挥了重要作用,帮助开发者将横切关注点(如日志记录、权限控制、事务管理)与业务逻辑分离。在 Spring AOP 中

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void logBeforeMethod() {
        System.out.println("方法执行前记录日志");
    }
}

通过 @Aspect@Before 注解,定义了一个在指定包内所有方法执行前记录日志的切面,实现了日志记录的横切关注点与业务逻辑的解耦