java高级——注解和反射
java高级——注解和反射
- 前情提要
- 一、注解
- 1. 什么是注解
- 2. 内置注解
- 3. 元注解:exclamation::exclamation::exclamation:
- 3.1 @Target(限定注解可以应用的目标元素类型,比如只能出现在class上)
- 3.2 @Retention(指定注解的保留策略,即注解在什么级别可用,一般都是RUNTIME)
- 3.3 Documented(指示注解应该被包含在Javadoc中)
- 3.4 @Inherited(可以被子类继承)
- 3. 5. @Repeatable (Java 8+允许在同一元素上多次使用相同的注解)
- 4. 自定义注解
- 二、反射
- 1. 反射的基本概念
- 2. 反射的核心类
- 3. 获取Class对象的三种方式
- 4. 反射的基本操作
- 4.1 创建对象
- 4.2 获取和操作字段
- 4.3 调用方法
- 5. 反射的应用场景
- 6. 反射的优缺点
- 7. 反射的真实场景开发
- 总结
前情提要
上一篇文章我们仔细的研究了高阶函数、stream流,困扰了多年的问题解决了,怎么能自定义一个函数式接口(甚至都不知道那叫做函数式接口)。
java高级——高阶函数、stream流
一、注解
1. 什么是注解
-
注解(Annotation)是从Java5.0开始引入的技术。
-
Annotation的作用
📌 不是程序本身,可以对程序做出解释(类似于注释)
📌 可以被其它程序(比如:编译器)读取 -
Annotation的格式
📌“@注释名”,还可以添加一些参数,例如:@SuppressWarnings(value=“unchecked”) -
Annotation可以使用在哪些地方
📌普遍使用在class、method、interface、field。
2. 内置注解
🔵内置注解一般是Java中自带的且比较常见的注解。
- @Override:修辞方法,表示
重写
超类中的另一个方法声明,在继承和实现类中非常常见。
- @Deprecated:修辞方法,表示该方法已经不推荐使用了,相当于
过时
。
- @SuppressWarnings:
消除警告
信息,比如说一些写法过时或者说不当,编译器会进行提示,可以用这个注解干掉,但是❌不推荐使用,这样会让程序可能出现未知的bug🐛。
上面三个是Java中内置注解比较常见的,当然在spring家族中,我们肯定在@GetMapping、@PostMapping等也很熟悉,这一篇文章就让你学会怎么在Java中自定义注解。
3. 元注解❗️❗️❗️
元注解就是修饰注解的注解
,说白了就是给自定义注解所提供的。
3.1 @Target(限定注解可以应用的目标元素类型,比如只能出现在class上)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {ElementType[] value();
}
📌ElementType取值:
-
TYPE:类、接口、枚举
-
FIELD:字段
-
METHOD:方法
-
PARAMETER:参数
-
CONSTRUCTOR:构造器
-
LOCAL_VARIABLE:局部变量
-
ANNOTATION_TYPE:注解类型
-
PACKAGE:包
-
TYPE_PARAMETER:类型参数(Java 8+)
-
TYPE_USE:类型使用(Java 8+)
✨示例
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation {}
3.2 @Retention(指定注解的保留策略,即注解在什么级别可用,一般都是RUNTIME)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {RetentionPolicy value();
}
📌RetentionPolicy取值:
-
SOURCE:仅源码级别,编译时丢弃
-
CLASS:编译时保留,但JVM不加载(默认)
-
RUNTIME:运行时保留,可通过反射读取
✨示例
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {}
3.3 Documented(指示注解应该被包含在Javadoc中)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
✨示例
@Documented
public @interface MyAnnotation {}
3.4 @Inherited(可以被子类继承)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
特点:
-
仅对类注解有效
-
接口上的注解不会被实现类继承
-
方法注解不会被重写方法继承
3. 5. @Repeatable (Java 8+允许在同一元素上多次使用相同的注解)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {Class<? extends Annotation> value();
}
✨示例
// 1. 定义容器注解
public @interface Schedules {Schedule[] value();
}// 2. 使用@Repeatable
@Repeatable(Schedules.class)
public @interface Schedule {String time();
}// 3. 使用方式
@Schedule(time = "morning")
@Schedule(time = "evening")
public void doSomething() {}
4. 自定义注解
import java.lang.annotation.*;@Target(ElementType.METHOD) // 可用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
@Documented // 包含在Javadoc中
public @interface MethodLogger {// 日志级别,默认为INFOLogLevel level() default LogLevel.INFO;// 是否记录方法参数boolean logParameters() default true;// 是否记录返回值boolean logResult() default false;// 超时警告阈值(毫秒)long timeoutThreshold() default -1;
}enum LogLevel {DEBUG, INFO, WARN, ERROR
}
public class UserService {@MethodLogger(level = LogLevel.DEBUG, logResult = true)public User getUserById(Long id) {// 方法实现}@MethodLogger(timeoutThreshold = 500)public void updateUser(User user) {// 方法实现}
}
上面就是一个自定义注解的例子,定义注解还是相对简单的,重点还是怎么去使用注解,要使用注解就需要了解反射机制,先不要着急,让我们来了解一下Java中的反射机制。
二、反射
反射(Reflection)是Java语言的一个强大特性,它允许程序在运行时动态地获取类的信息
并操作类或对象的属性、方法和构造器
。反射机制是Java被视为动态语言的关键特性之一。
用大白话来说,反射允许我们在程序运行的过程中读取某个类的详细信息,同时也可以调用这个类的方法或者访问其中的属性。因为在一般的编程中,我们都是指定某个具体的类来操作其中的方法和属性,所属的位置是调用者层面
,✨反射则是站在开发者层面
,通过开发者编写的代码或者提供的某个信息,找到对应的类并进行操作,举个例子。
springmvc中,如果一个controller类出现了两个相同路径的注解比如GetMapping(“aaa”),这时候编译器是会报错的,因为不允许两个相同的请求路径,那底层是怎么实现的呢?虽然没有使用反射机制,但实现的原理和过程和反射类似。
注解的检查是在启动的时候就完成的,具体实现的类在RequestMappingHandlerMapping
中,它是扫描的所有的controller和RequestMapping,最后注册
RequestMapping的时候进行冲突检测
。这里使用扫描的手段获取了想要得到的对象,而在反射中,我们根据对应的类名创建出想要的对象
,达到相同的目的。
1. 反射的基本概念
-
获取任意一个类的Class对象
-
获取类的所有成员变量💡、方法💡、构造器💡等信息
-
创建对象、调用方法、访问或修改字段值
-
动态代理
2. 反射的核心类
-
Class
类:代表类的实体 -
Field
类:代表类的成员变量 -
Method
类:代表类的方法 -
Constructor
类:代表类的构造方法
这里面最常用的还是Field类,一般我们都是用于操作对象上面的属性,如果熟悉EasyExcel框架的就知道,导出是可以通过@Excel
注解实现的,底层就是访问的Field类来获取对应的信息。而方法则一般用于框架开发和一些特殊的机制需要调用类上面的方法。
3. 获取Class对象的三种方式
// 1. 通过类名.class获取
Class<?> clazz1 = String.class;// 2. 通过对象.getClass()获取
String str = "Hello";
Class<?> clazz2 = str.getClass();// 3. 通过Class.forName()获取(最常用)
Class<?> clazz3 = Class.forName("java.lang.String");
4. 反射的基本操作
4.1 创建对象
// 获取Class对象
Class<?> clazz = Class.forName("com.example.Person");// 使用无参构造器创建对象
Object obj1 = clazz.newInstance(); // 已过时,Java9+
Object obj2 = clazz.getDeclaredConstructor().newInstance();// 使用有参构造器创建对象
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object obj3 = constructor.newInstance("张三", 25);
注意哈,在后期的Java版本中逐渐的淘汰了newInstance这个方法,最好使用第二种。❗️
4.2 获取和操作字段
// 获取Class对象
Class<?> clazz = Class.forName("com.example.Person");
Object obj = clazz.getDeclaredConstructor().newInstance();// 获取公共字段
Field publicField = clazz.getField("publicFieldName");// 获取所有字段(包括私有)
Field[] fields = clazz.getDeclaredFields();
Field privateField = clazz.getDeclaredField("privateFieldName");// 访问字段值
privateField.setAccessible(true); // 对私有字段需要设置可访问
Object value = privateField.get(obj);// 修改字段值
privateField.set(obj, "newValue");
💡这是我们最常用的一种写法,获取到对象的属性,可以读取对应的值,介绍完基本的概念之后会将一个真实开发的例子(公式的计算)。
4.3 调用方法
// 获取Class对象
Class<?> clazz = Class.forName("com.example.Person");
Object obj = clazz.getDeclaredConstructor().newInstance();// 获取公共方法
Method publicMethod = clazz.getMethod("methodName", parameterTypes);// 获取所有方法(包括私有)
Method[] methods = clazz.getDeclaredMethods();
Method privateMethod = clazz.getDeclaredMethod("privateMethodName", parameterTypes);// 调用方法
privateMethod.setAccessible(true); // 对私有方法需要设置可访问
Object result = privateMethod.invoke(obj, args);
5. 反射的应用场景
📌框架开发:如Spring的IoC容器、Hibernate的ORM实现
📌动态代理:AOP编程的基础
📌注解处理:运行时通过反射读取注解信息
📌工具类开发:如BeanUtils、JSON序列化等
📌IDE功能:如代码提示、自动补全
6. 反射的优缺点
优点:
-
提高程序的灵活性和扩展性
-
可以在运行时动态获取类信息
-
可以实现动态创建对象和调用方法
缺点:
-
性能开销:反射操作比直接代码调用慢
-
安全限制:反射需要运行时权限
-
内部暴露:可以访问私有成员,破坏了封装性
-
代码复杂度:反射代码可读性较差,调试困难
7. 反射的真实场景开发
之前的公司中有一个需求是要定制工资的计算公式,根据公式计算薪资结果,那么计算公式定义的时候最重要的一个概念是动态参数
和动态的函数
,也就是后期可能会因为业务需求增加参数和函数
,那总不能改代码吧。所以我们利用了注解,尽量让代码的改动变小,大致的实现思路如下:
代码中有一个专门的package是calculateDomain,这里面定义了所有的参数类,如果对象中的某个属性需要定义为计算参数,在属性上添加注解(TableColumnMapping)即可。
/*** 标注可以映射参数的字段*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TableColumnMapping {String name() default "";
}// 使用示例@TableColumnMapping(name = "岗位工资标准(jobSalaryStandard)")private String jobSalaryStandard;
添加完注解之后,我们继续定义一个常量,记录类名和一些其它的配置信息,如下:
public static final List<Map<String, String>> FACTOR_MAPPING_TABLE_LIST = Arrays.asList(new HashMap<>() {{put("id", "SalaryInfo");put("name", "薪酬信息(salary_info)");put("class", "com.tpehr.calculate.domain.SalaryInfo");}})
之后定义方法获取对应的注解信息,然后返回给前台。
private static List<Map<String, String>> getColumnsByClassName(String className) throws ClassNotFoundException {List<Map<String, String>> resList = new ArrayList<>();// 解析出所有标记注解的字段Class clazz = Class.forName(className);Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {if (field.isAnnotationPresent(TableColumnMapping.class)) {Map<String, String> map = new HashMap<>();TableColumnMapping tableColumnMapping = field.getAnnotation(TableColumnMapping.class);map.put("id", field.getName());map.put("name", tableColumnMapping.name());resList.add(map);}}return resList;}
核心的代码如上,因为返回给前台的信息就是一个大的工资类型,比如社保,社保下面还会有很多详细的计算项,所以加了一层配置常量,后期只需要维护对应的常量和Java对象即可。
总结
关于自定义注解和反射到这里就结束了,我们目前只需要掌握反射的基本使用方式就可以了,已经能适用于大多数场景。