Java高级特性:单元测试、反射、注解、动态代理
Java作为一种强大的面向对象编程语言,提供了多种高级特性来提升开发效率和灵活性。这些特性包括单元测试、反射、注解和动态代理,它们在现代Java开发中扮演着重要角色。
1. 单元测试
单元测试是软件开发中的关键实践,用于验证代码的单个单元(如方法或类)是否按预期工作。在Java中,JUnit是最流行的单元测试框架,它简化了测试编写和执行过程。
- 为什么重要:单元测试能及早发现错误,提高代码质量,并支持重构。测试覆盖率可以用数学公式表示。
- 核心元素:JUnit使用注解(如
@Test)标记测试方法,并提供断言方法(如assertEquals)来验证结果。 - 示例代码:以下是一个简单的JUnit测试示例,测试一个计算器类的加法方法。
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;public class CalculatorTest {@Testpublic void testAdd() {Calculator calculator = new Calculator();int result = calculator.add(2, 3);assertEquals(5, result); // 验证结果是否等于5}
}class Calculator {public int add(int a, int b) {return a + b;}
}
在这个示例中:
@Test注解标记了测试方法。assertEquals用于比较预期值和实际值。- 运行测试时,如果
add方法返回5,测试通过;否则失败。
示例
• 某个系统,有多个业务方法,请使用Junit单元测试框架,编写测试代码,完成对这些方法的正确性测试。
具体步骤
① 将Junit框架的jar包导入到项目中(注意:IDEA集成了Junit框架,不需要我们自己手工导入了)
② 为需要测试的业务类,定义对应的测试类,并为每个业务方法,编写对应的测试方法(必须:公共、无参、无返回值)
③ 测试方法上必须声明@Test注解,然后在测试方法中,编写代码调用被测试的业务方法进行测试;
④ 开始测试:选中测试方法,右键选择“JUnit运行”,如果测试通过则是绿色;如果测试失败,则是红色
2. 反射
反射(Reflection)是Java的一种机制,允许程序在运行时检查和修改类、对象、方法和字段的结构。它通过java.lang.reflect包实现,常用于框架开发(如Spring)和动态加载类。
(一) 认识反射(Reflection)
反射是指在程序运行时,能够动态地获取、检查和操作类、对象、方法、属性等程序自身结构的能力。它允许程序在运行时“观察”和修改自身的状态和行为,而无需在编译时知道具体的类型信息。简单来说,反射就是程序在运行时“认识自己”的能力。
核心思想是:通过类的元数据(Metadata),在运行时动态地获取类的信息,并对其成员进行操作。
理解:反射就是加载类并允许以编程的方式解剖类中的各种成分(成员变量,方法,构造器等)
- 为什么重要:反射提供了动态性,例如在未知类名时创建对象或调用方法。但需注意性能开销,因为反射操作通常比直接调用慢(时间复杂度约为 O(n),其中 n 是反射调用的复杂度)。
- 核心类:主要类包括
Class(表示类)、Method(表示方法)、Field(表示字段)等。 - 示例代码:以下代码展示如何使用反射获取类的信息并调用方法。
import java.lang.reflect.Method;public class ReflectionExample {public static void main(String[] args) throws Exception {// 获取String类的Class对象Class<?> stringClass = String.class;// 获取toUpperCase方法并调用Method toUpperCaseMethod = stringClass.getMethod("toUpperCase");String str = "hello";String result = (String) toUpperCaseMethod.invoke(str);System.out.println(result); // 输出 "HELLO"}
}
在这个示例中:
Class.forName()或类名.class获取Class对象。getMethod获取方法,invoke调用方法。- 反射常用于动态代理和注解处理。
(二) 获取类及其成分并操作
(1) 获取类 (Class)
通常可以通过以下方式获取一个类的 Clas 对象:
- 使用
类名.class(如:String.class) - 通过对象调用
getClass()方法 (如:"hello".getClass()) - 使用
Class.forName("全限定类名")(如:Class.forName("java.lang.String"))
// 示例:获取String类的Class对象
Class<?> stringClass = Class.forName("java.lang.String");
(2) 获取类中的成分
一旦获取了 Class 对象,就可以进一步获取其内部的成员:
- 字段 (Field)
// 获取所有公共字段 Field[] publicFields = stringClass.getFields(); // 获取所有字段(包括私有) Field[] allFields = stringClass.getDeclaredFields(); // 获取特定字段 Field specificField = stringClass.getDeclaredField("value"); - 方法 (Method)
// 获取所有公共方法 Method[] publicMethods = stringClass.getMethods(); // 获取所有方法(包括私有) Method[] allMethods = stringClass.getDeclaredMethods(); // 获取特定方法 Method charAtMethod = stringClass.getMethod("charAt", int.class); - 构造器 (Constructor)
// 获取所有公共构造器 Constructor<?>[] publicConstructors = stringClass.getConstructors(); // 获取所有构造器 Constructor<?>[] allConstructors = stringClass.getDeclaredConstructors(); // 获取特定构造器(如String的byte[]构造器) Constructor<?> byteConstructor = stringClass.getConstructor(byte[].class);
(3) 操作类成分
获取到这些成员后,可以对其进行操作:
- 创建对象实例
// 使用无参构造器创建String实例 Object strInstance = stringClass.newInstance(); // 使用带参构造器 Constructor<?> constructor = stringClass.getConstructor(byte[].class); String strFromBytes = (String) constructor.newInstance(new byte[]{65, 66, 67}); - 调用方法
// 调用charAt方法 Method charAt = stringClass.getMethod("charAt", int.class); char result = (char) charAt.invoke("ABC", 1); // 返回 'B' - 访问/修改字段值
// 获取并修改私有字段(需要设置可访问性) Field valueField = stringClass.getDeclaredField("value"); valueField.setAccessible(true); // 突破封装访问限制 char[] oldValue = (char[]) valueField.get("Hello"); valueField.set("Hello", new char[]{'W', 'o', 'r', 'l', 'd'}); - 注解处理
// 获取类上的注解 Annotation[] annotations = stringClass.getAnnotations();
具体示例
package demo1reflect;import lombok.Data;@Data
public class Dog {private String name;private int age;private String hobby;private Dog() {}public Dog(String name) {this.name = name;}public Dog(String name, int age) {this.name = name;this.age = age;}private void eat() {System.out.println("吃吃吃吃吃");}public String eat(String food){System.out.println("吃吃吃吃吃:" + food);return "yammy";}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String getHobby() {return hobby;}public void setHobby(String hobby) {this.hobby = hobby;}
}
package demo1reflect;import org.junit.Test;import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Objects;public class ReflectDome2 {//1.获取Class对象@Testpublic void getClassInfo(){Class c1 = Dog.class;System.out.println(c1.getClasses());//类名的全类名System.out.println(c1.getSimpleName());//类名}//2.获取类的构造方法对象并对其进行操作@Testpublic void getConstructorInfo() throws Exception {//获取类的构造方法对象//1、获取Class对象Class c1 = Dog.class;//2、获取构造方法对象Constructor[] constructor = c1.getDeclaredConstructors();for (Constructor c : constructor) {System.out.println(c.getName() + "(" + c.getParameterCount() + ")");}//3. 获取单个构造方法对象Constructor c2 = c1.getDeclaredConstructor();//获取无参构造方法对象System.out.println(c2.getName() + "(" + c2.getParameterCount() + ")");Constructor c3 = c1.getDeclaredConstructor(String.class, int.class);//获取有参构造方法对象System.out.println(c3.getName() + "(" + c3.getParameterCount() + ")");//4. 获取的作用:创建对象//创建无参对象:私有的-暴力反射!c2.setAccessible(true);//暴力反射,绕过访问权限修饰符Dog d1 = (Dog) c2.newInstance();System.out.println(d1);//创建有参对象Dog d2 = (Dog) c3.newInstance("小花", 5);System.out.println(d2);}//3. 获取类的成员变量对象并对其进行操作@Testpublic void test03() throws Exception {//1. 获取类对象Class c1 = Dog.class;//2. 获取成员变量对象Field[] fields= c1.getDeclaredFields();for (Field field : fields){System.out.println(field.getName() + "(" + field.getType().getName() + ")");}//3.获取单个成员变量对象Field field = c1.getDeclaredField("hobby");System.out.println(field.getName() + "(" + field.getType().getName() + ")");Field field1 = c1.getDeclaredField("age");System.out.println(field1.getName() + "(" + field1.getType().getName() + ")");//4. 获取类的成员方法对象的目的依然是取值和设置值Dog d = new Dog("旺财", 5);field.setAccessible( true);//暴力反射,hobby属性是私有的,所以需要设置field.set(d, "看家");System.out.println(d);String hobby = (String) field.get(d);//强转的原因: get方法返回的是Object类型System.out.println(hobby);}//4.获取类的成员方法对象并对其进行操作@Testpublic void test4() throws Exception {//1.获取类本身Class c1 = Dog.class;//2.获取方法对象Method[] methods = c1.getDeclaredMethods();for (Method method : methods) {System.out.println(method.getName() + "(" + method.getParameterTypes() + ")");}//3.获取单个方法对象Method m = c1.getDeclaredMethod("eat");//获取无参数eat方法对象System.out.println(m.getName() + "(" + m.getParameterTypes() + ")");Method m2 = c1.getDeclaredMethod("eat", String.class);//获取有参数eat方法对象System.out.println(m2.getName() + "(" + m2.getParameterTypes() + ")");//4.调用方法对象的作用依然是执行Dog d1 = new Dog("金毛",3);m.setAccessible( true);m.invoke(d1);//调用无参数方法对象,相当于d1.eat();Object obj = m2.invoke(d1,"骨头");//调用有参数方法对象,相当于d1.eat("骨头");System.out.println(obj);}
}
(三) 反射的作用和应用场景
基本作用:可以得到一个类的全部成分然后操作;可以破坏封装性;可以绕过泛型的约束!
最重要的用途:适合做Java的框架,基本上,主流的框架都会基于反射设计出一些通用的功能。
1、作用
- 动态性:程序可以在运行时动态加载类、创建对象、调用方法,提高灵活性。
- 框架开发:是各种框架(如Spring、Hibernate)实现依赖注入、AOP等高级特性的基础。
- 通用代码:编写可处理任意类型的通用工具或库(如序列化/反序列化库)。
- 测试工具:单元测试框架(如JUnit)利用反射查找并执行测试方法。
2、应用场景
IoC (控制反转) / DI (依赖注入) 容器 框架通过反射扫描类路径,读取注解(如
@Component,@Autowired),动态创建Bean并注入依赖。// 伪代码:根据配置创建实例 Class<?> beanClass = Class.forName(beanClassName); Object beanInstance = beanClass.newInstance(); // 查找并注入依赖字段 for (Field field : beanClass.getDeclaredFields()) {if (field.isAnnotationPresent(Autowired.class)) {field.setAccessible(true);Object dependency = ...; // 获取依赖对象field.set(beanInstance, dependency);} }ORM (对象关系映射) 框架 如Hibernate,通过反射获取实体类的字段信息,动态生成SQL语句,并将查询结果映射回对象。
// 伪代码:将结果集映射到对象 ResultSet rs = ...; User user = new User(); for (Field field : User.class.getDeclaredFields()) {field.setAccessible(true);field.set(user, rs.getObject(field.getName())); }动态代理 (Proxy) 在运行时创建实现特定接口的代理类,用于实现AOP(面向切面编程),如日志记录、事务管理。
// Java动态代理示例 InvocationHandler handler = (proxy, method, args) -> {System.out.println("调用方法: " + method.getName());return method.invoke(target, args); }; MyInterface proxy = (MyInterface) Proxy.newProxyInstance(MyInterface.class.getClassLoader(),new Class[]{MyInterface.class},handler);配置文件驱动的对象创建 根据配置文件中的类名动态创建对象并调用方法。
// 读取配置文件 String className = config.getProperty("processor.class"); String methodName = config.getProperty("processor.method"); // 反射创建并调用 Class<?> processorClass = Class.forName(className); Object processor = processorClass.newInstance(); Method method = processorClass.getMethod(methodName); method.invoke(processor);插件系统 允许程序在运行时加载并执行未知的插件模块。
// 加载插件JAR,获取入口类并执行
URLClassLoader loader = new URLClassLoader(new URL[]{new File("plugin.jar").toURI().toURL()});
Class<?> pluginClass = loader.loadClass("com.example.Plugin");
PluginInterface plugin = (PluginInterface) pluginClass.newInstance();
plugin.execute();
绕过泛型类型约束
在 Java 中,反射(Reflection) 确实可以绕过泛型(Generics)的类型约束,这主要是由于 Java 泛型的实现机制——类型擦除(Type Erasure)。
一、类型擦除机制
Java 泛型在编译期进行类型检查,但在运行时会将泛型类型信息擦除,替换为原始类型(通常是
Object或边界类型)。例如:List<String> list = new ArrayList<>();编译后,
List<String>会被擦除为List<Object>(实际是原始类型List)。因此运行时无法直接获取泛型的具体类型参数(如String)。
二、反射绕过泛型约束的原理
由于类型擦除,运行时集合中存储的实际是
Object类型。通过反射操作集合时,可以绕过编译期的泛型检查,向集合插入非泛型指定类型的对象。例如:List<String> stringList = new ArrayList<>(); stringList.add("Hello");// 反射获取 add 方法 Method addMethod = ArrayList.class.getMethod("add", Object.class); addMethod.invoke(stringList, 123); // 插入 Integer 类型此时,
stringList中既包含String类型,也包含Integer类型,绕过了泛型约束。
三、具体操作步骤
获取集合的 Class 对象
通过getClass()方法获取运行时类信息:Class<?> listClass = stringList.getClass();获取目标方法
泛型擦除后,add方法的参数类型实际为Object:Method addMethod = listClass.getMethod("add", Object.class);通过反射调用方法
直接插入非泛型指定类型的对象:addMethod.invoke(stringList, 123); // 插入整数
四、风险与问题
运行时异常
后续按泛型类型操作集合时可能抛出ClassCastException:for (String s : stringList) {// 遍历到 Integer 时会抛出异常 }破坏类型安全
泛型的核心目的是保证类型安全,反射绕过约束会破坏这一机制。
五、实际应用场景
虽然不推荐,但在某些特殊场景下可能有用,例如:
- 框架开发:如 JSON 序列化库需动态处理不同类型。
- 兼容旧代码:适配遗留的非泛型代码。
总结
反射绕过泛型是 Java 类型擦除机制的副作用。尽管技术上可行,但会破坏类型安全,需谨慎使用。正确做法是:
- 优先使用泛型保证类型安全;
- 如需动态类型,改用
List<Object>或设计多态结构。
3、注意事项
- 性能开销:反射操作通常比直接代码调用慢,因为涉及动态解析和类型检查。
- 安全限制:可能突破封装性(访问私有成员),需谨慎使用并注意安全管理器设置。
- 代码可读性:过度使用反射可能导致代码难以理解和维护。
总之,反射是一个强大的工具,它为程序提供了高度的灵活性和动态能力,尤其在框架开发和需要处理未知类型的场景中不可或缺。但需权衡其带来的性能损失和复杂性,在适当场景下使用。
3. 注解
(一) 注解概述
注解(Annotation)是 Java 提供的一种元数据机制,用于为程序元素(如类、方法、变量、参数等)添加额外的说明信息。这些信息本身不会直接影响程序的逻辑,但可以被编译器、开发工具或其他程序读取并利用。
- 核心作用: 提供关于程序代码的元数据描述。
- 编译时处理: 编译器可以利用注解信息进行编译检查(如
@Override)或生成辅助代码/文件(如APT)。 - 运行时处理: 通过反射机制读取注解信息,实现运行时动态行为(如Spring框架的依赖注入)。
- 文档化:
@Deprecated等注解有助于生成文档。
- 为什么重要:注解简化了配置和元数据管理,例如在JUnit中标记测试方法,或在Spring中依赖注入。常见的内置注解包括
@Override、@Deprecated和@SuppressWarnings。 - 自定义注解:开发者可以定义自己的注解,通过
@interface关键字。 - 示例代码:以下展示如何定义和使用一个自定义注解。
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 ImportantMethod {
}// 使用注解的类
public class AnnotationExample {@ImportantMethodpublic void criticalOperation() {System.out.println("执行重要操作");}public static void main(String[] args) {AnnotationExample example = new AnnotationExample();example.criticalOperation(); // 输出 "执行重要操作"}
}
在这个示例中:
@ImportantMethod是一个自定义注解。@Retention指定注解保留到运行时,以便反射处理。- 注解可以被反射API读取,用于动态行为。
(二) 元注解
元注解(Meta-Annotation)是用于注解其他注解的注解。它们定义了被注解的注解的行为和适用范围。Java 提供了几个核心的元注解(位于java.lang.annotation包):
@Retention: 指定被注解的注解的保留策略(即生命周期)。RetentionPolicy.SOURCE: 仅存在于源代码中,编译时被丢弃(如@Override,@SuppressWarnings)。RetentionPolicy.CLASS: 保留在 class 文件中,但不会被加载到 JVM(默认策略,较少用)。RetentionPolicy.RUNTIME: 保留在 class 文件中,并被加载到 JVM,可在运行时通过反射读取(框架常用)。[即一直保留到运行阶段!]
@Target: 指定被注解的注解可以应用于哪些程序元素。- 常用元素类型:
ElementType.TYPE(类/接口/枚举),ElementType.FIELD(字段),ElementType.METHOD(方法),ElementType.PARAMETER(参数),ElementType.CONSTRUCTOR(构造器),ElementType.LOCAL_VARIABLE(局部变量),ElementType.ANNOTATION_TYPE(注解类型),ElementType.PACKAGE(包),ElementType.TYPE_PARAMETER(类型参数-JDK8),ElementType.TYPE_USE(类型使用-JDK8)。
- 常用元素类型:
@Documented: 指明被注解的注解应该被包含在 Javadoc 文档中。@Inherited: 指明被注解的注解具有继承性。如果父类使用了该注解,子类默认也会继承这个注解(仅对类注解有效)。@Repeatable(JDK8+): 指明被注解的注解可以在同一个元素上重复使用。
(三) 自定义注解
开发者可以根据需要定义自己的注解类型。定义格式类似于接口,但使用 @interface 关键字:
@Retention(RetentionPolicy.RUNTIME) // 元注解:运行时保留
@Target(ElementType.METHOD) // 元注解:只能注解方法
public @interface MyCustomAnnotation {// 定义注解的属性(成员变量)String value() default "defaultValue"; // 带默认值的属性int priority() default 0; // 带默认值的属性String[] tags() default {}; // 数组类型属性
}
- 注解的属性声明类似于接口的方法,但没有参数,可以有默认值 (
default ...)。 - 属性的类型有限制:基本类型、
String、Class、枚举类型、注解类型,以及这些类型的数组。 - 使用注解时,通过
@AnnotationName(propertyName=value)格式指定属性值。如果注解只有一个名为value的属性且需要赋值时,可以省略value=直接写值。如果所有属性都有默认值,可以不指定任何属性值 (@AnnotationName)。
(四) 注解的解析
注解本身只是元数据,其功能需要通过解析器来实现。解析方式主要有两种:
- 编译时解析 (APT - Annotation Processing Tool):
- 在编译阶段,由特定的注解处理器 (
javax.annotation.processing.Processor) 处理源代码或 class 文件中的注解。 - 处理器可以读取注解信息,生成新的源代码、资源文件或编译错误/警告。
- 处理过程在 Javac 编译时自动触发。处理器生成的代码会参与后续的编译。
- 特点: 不修改原有代码,生成新代码。常用于生成样板代码(如 Builder 模式)、验证约束(如检查是否实现了接口)、生成配置文件等。例如 Lombok 库的核心原理。
- 在编译阶段,由特定的注解处理器 (
- 运行时解析 (Reflection):
- 在程序运行时,通过 Java 的反射 API (
java.lang.reflect) 获取注解信息。 - 核心类:
AnnotatedElement接口(Class,Method,Field,Constructor等都实现了它),提供getAnnotation(Class),isAnnotationPresent(Class),getAnnotations()等方法。 - 特点: 需要注解保留策略为
RUNTIME。常用于框架的动态行为,如 Spring 的依赖注入 (@Autowired)、事务管理 (@Transactional)、Web 路由 (@RequestMapping) 等。
- 在程序运行时,通过 Java 的反射 API (
AnnotatedElement 接口解析
AnnotatedElement 是 Java 反射 API 中的核心接口,用于处理注解(Annotation)。它定义了一套标准方法来访问和操作类、方法、字段等元素上的注解。许多核心类如
Class、Method、Field和Constructor都实现了这个接口,使得开发者可以统一地获取和检查注解信息。1. AnnotatedElement 接口的作用
AnnotatedElement 接口的主要目的是提供一种通用的方式来访问元素的注解。在 Java 中,注解用于添加元数据到代码中,例如标记方法为测试用例或指定字段的序列化规则。通过实现这个接口,任何支持注解的元素(如类、方法等)都能通过反射机制被查询和处理。这简化了注解的解析过程,无需为每种元素类型编写重复代码。
2. 核心方法解析
AnnotatedElement 接口定义了多个方法,用于获取和检查注解。以下是主要方法的详细说明:
getAnnotation(Class<T> annotationClass):
这个方法用于获取指定类型的注解实例。参数annotationClass是注解的 Class 对象。如果元素上存在该注解,则返回注解实例;否则返回null。例如,检查一个方法是否使用了@Override注解:Method method = ... // 获取方法的实例 Override overrideAnnotation = method.getAnnotation(Override.class); if (overrideAnnotation != null) {System.out.println("该方法被 @Override 注解标记"); }
isAnnotationPresent(Class<? extends Annotation> annotationClass):
这个方法检查元素上是否存在指定类型的注解。它返回一个布尔值:true表示注解存在,false表示不存在。这个方法比getAnnotation更轻量级,适合快速检查:Field field = ... // 获取字段的实例 if (field.isAnnotationPresent(Deprecated.class)) {System.out.println("该字段已废弃,请勿使用"); }
getAnnotations():
这个方法返回元素上的所有注解(包括继承的注解,如果有)。返回值是一个Annotation[]数组。如果元素没有注解,则返回空数组。这适用于需要遍历所有注解的场景:Class<?> clazz = ... // 获取类的实例 Annotation[] annotations = clazz.getAnnotations(); for (Annotation ann : annotations) {System.out.println("注解类型: " + ann.annotationType()); }其他相关方法:
getDeclaredAnnotations():返回直接声明在元素上的注解(不包括继承的注解)。getAnnotationsByType(Class<T> annotationClass):用于获取重复注解(Java 8 引入),返回指定类型的所有注解实例。getDeclaredAnnotationsByType(Class<T> annotationClass):类似,但只包括直接声明的注解。3. 实现类和实际应用
AnnotatedElement 接口由多个 Java 核心类实现,包括:
Class:用于访问类或接口上的注解。Method:用于访问方法上的注解。Field:用于访问字段上的注解。Constructor:用于访问构造器上的注解。Package:用于访问包上的注解。这些实现使得注解解析具有一致性。例如,在自定义注解处理器中,你可以通过反射遍历一个类的所有元素:
import java.lang.annotation.*; import java.lang.reflect.*;// 定义一个自定义注解 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) public @interface MyTest {String value();double height() default 180.2;String[] address(); }// 使用注解的示例类 @MyTest(value = "wjk", address = "beijing") public class Demo {@MyTest(value = "wy", address = {"beijing","boston"})public void go(){} }// 解析注解的主类 public class AnnotationDome2 {//解析注解@Testpublic void praseClass() throws Exception {//获取类对象Class c = Demo.class;//判断类对象上是否有MyTest注解if (c.isAnnotationPresent(MyTest.class)) {//获取类对象上的MyTest注解MyTest myTest = (MyTest) c.getDeclaredAnnotation(MyTest.class);//获取注解的属性值System.out.println(myTest.value());System.out.println(myTest.height());System.out.println(Arrays.toString(myTest.address()));}}@Testpublic void praseMethod() throws Exception{Class c = Demo.class;//获取Demo类对象//获取Demo类对象中的方法对象Method method = c.getMethod("go");//判断方法上是否有MyTest注解if (method.isAnnotationPresent(MyTest.class)){//获取方法上的MyTest注解MyTest myTest = method.getDeclaredAnnotation(MyTest.class);//获取注解的属性值System.out.println(myTest.value());System.out.println(myTest.height());System.out.println(Arrays.toString(myTest.address()));}} }运行上述代码,输出可能为:
wjk 180.2 [beijing] wy 180.2 [beijing, boston]4. 注意事项和最佳实践
- 性能考虑:反射操作(如获取注解)可能较慢,应在必要时使用,例如在框架初始化时缓存结果。
- 注解保留策略:确保注解的
@Retention设置为RetentionPolicy.RUNTIME,否则反射无法访问。- 继承性:默认情况下,注解不会被继承;如果需要,使用
@Inherited元注解。- 错误处理:调用这些方法时,如果元素不可访问(如私有字段),可能抛出
SecurityException,需适当处理。
(五) 注解的作用与应用场景
- 编译检查:
@Override,@FunctionalInterface帮助编译器检查代码的正确性。 - 代码生成: APT 用于自动生成代码,减少重复劳动(如 Dagger, Lombok, MapStruct)。
- 框架配置: 简化配置,代替 XML 配置文件(如 Spring Boot 的
@SpringBootApplication,@RestController,@Bean)。 - 运行时行为控制: 框架根据运行时解析的注解实现 AOP、事务、权限控制等(如 Spring, Hibernate)。
- 文档生成:
@Deprecated,@see,@param,@return等用于 Javadoc。 - 代码分析: 静态代码分析工具(如 FindBugs, SonarQube)可以利用注解信息。
- 测试: JUnit 的
@Test,@Before,@After等标记测试方法。 - 序列化/反序列化: Jackson 的
@JsonProperty,@JsonIgnore控制 JSON 处理。 - 依赖注入: Guice, Spring 的
@Inject,@Autowired。 - 标记接口替代: 比标记接口更灵活(如
@Entity代替Entity接口)。 - 约束校验: Bean Validation (JSR 303/349/380) 的
@NotNull,@Size,@Pattern等。
总结: Java 注解是一种强大的元数据机制,通过元注解定义其行为,允许开发者创建自定义注解。注解的生命周期(@Retention)和适用范围(@Target)由元注解控制。注解的功能依赖于编译时(APT)或运行时(反射)的解析器。
4. 动态代理
动态代理(Dynamic Proxy)是Java反射机制的一部分,允许在运行时创建代理对象,用于拦截方法调用。它常用于AOP(面向切面编程)和日志记录等场景。
- 为什么重要:动态代理实现了间接调用,增强了代码的灵活性和可维护性。例如,在代理模式中,时间复杂度可能为 O(1) 如果代理直接委托,但添加逻辑会增加开销。
- 核心类:
java.lang.reflect.Proxy类用于创建代理对象,InvocationHandler接口处理调用。 - 示例代码:以下代码展示如何创建一个动态代理来记录方法调用。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;// 定义接口
public interface StarServer {void sing(String name);String dance();
}// 实现接口的类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Star implements StarServer{private String name;@Overridepublic void sing(String name) {System.out.println(this.name+"正在唱歌"+name);}@Overridepublic String dance() {System.out.println(this.name+"正在跳舞...");return "Thanks";}
}// InvocationHandler实现-代理工厂:创建代理并实现方法
//代理工具类:负责创建代理
public class ProxyUtil {//创建一个Proxy对象,代理public static StarServer createProxy(Star s){/*** 参数一:用于执行用哪个类加载器去加载生成的代理类* 参数二:用于指定代理类需要实现的接口:明星类实现了接口,所以代理类必须实现这个接口* 参数三:用于指定代理类需要如何去代理(代理要做的事情)*/StarServer proxy = (StarServer) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),//new Class[]{StarServer.class},s.getClass().getInterfaces(),new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//声明代理对象要干的事情//参数一:代理对象//参数二:正在被代理的方法//参数三:被代理的方法的参数String name = method.getName();if (name.equals("sing")){System.out.println("准备话筒...");}else if (name.equals("dance")){System.out.println("准备场地...");}//真正干活,找Star对象来执行被代理的行为:method.invoke(star,args);Object result = method.invoke(s, args);return result;}});return proxy;}
}//测试
public class Test {static void main(String[] args) {//创建一个Star对象Star star = new Star("yyqx");//为star创建一个代理对象StarServer proxy =ProxyUtil.createProxy(star);proxy.sing("Relax");System.out.println(proxy.dance());}
}
综合应用
这些技术常结合使用。例如:
- 在单元测试中,使用注解标记测试方法。
- 反射用于读取注解信息或动态创建对象。
- 动态代理在测试中模拟对象行为(如Mockito框架)。 整体上,它们提升了Java的灵活性和可扩展性。
总结:单元测试确保代码质量,反射提供运行时自省能力,注解简化元数据管理,动态代理实现方法拦截。掌握这些高级特性是Java开发者的关键技能,能显著提高开发效率和代码健壮性。实践中,建议优先使用框架(如JUnit、Spring)来简化实现。
Javase完结,后面会接着写Javaweb相关。
