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
注解,定义了一个在指定包内所有方法执行前记录日志的切面,实现了日志记录的横切关注点与业务逻辑的解耦