Java反射和注解简介
一、前言
首先我们要知道什么是注解和反射:
- 注解:代码里的特殊标记,这些标记可以在编译,类加载,或者运行时被读取。是JDK5.0引入的。
- 反射:反射是指在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性,这种动态获取信息以及动态调用对象方法的功能就称为Java的反射机制。
反射和注解是Java中许多框架底层常用的技术(如SpringBoot),在我们日常使用这些框架进行开发时,对注解肯定不陌生,注解具有强大的功能,在许多框架中使用注解可以快速进行开发,不过反射在日常开发中使用的并不多,反射机制是框架底层代码帮我们做的。因此,要想深度了解框架知识,必须要学习注解和反射。
本博客是在学习B站上尚学堂的课程进行总结的:【尚学堂】Java反射和注解
二、反射
2.1 反射介绍
Java反射机制就是在运行状态下,动态地获取一个类或者对象所有的方法和属性。反射可以将Java类中各个成分映射为一个个的Java对象。
反射的特点
- 优点:灵活性和扩展性
- 缺点:性能问题,反射是在运行过程中操作字节码文件实现的,要比直接使用源代码操作慢
对于反射性能问题,可能的原因
- 需要检查方法是否可见
- 需要校验参数
- JIT无法进行优化
总结:反射主要是用在对灵活性和扩展性要求比较高的框架中,普通代码不建议使用。
2.2 Class类介绍
要使用反射,首先要获取该类字节码文件所对应的Class对象,而解剖类中的各成分使用的就是Class类中的方法。
可以查询API文件找到Class类,java.lang.Class
可以看到类是没有公共构造方法的,因此不能通过new
的方式来创建Class
对象,获取Class
对象有以下三种方式
- 通过类直接获取Class
- 通过对象的getClass()获取
- 通过类路径获取
public static void main(String[] args) throws ClassNotFoundException {
// 方法一:直接通过类静态获取
Class class1 = User.class;
// 方法二:通过对象来进行获取
User user = new User();
Class class2 = user.getClass();
// 方法三:通过类路径
String classPath = "com.liu.ref_anno.User";
Class class3 = Class.forName(classPath);
}
Class对象的常用方法
public void test01() throws Exception {
// 获取一个类的结构信息
String path = "com.liu.ref_anno.User";
Class clazz = Class.forName(path);
// 获取类的完整名称,即com.liu.ref_anno.User
System.out.println(clazz.getName());
// 获取类名称,即User
System.out.println(clazz.getSimpleName());
// 获取类的父类
System.out.println(clazz.getSuperclass());
// 获取类的接口
System.out.println(Arrays.toString(clazz.getInterfaces()));
}
2.3 获取类中的成分
获取类的成分
- 构造方法
- 属性值
- 普通方法
2.3.1 获取Class的构造方法
一般用new
方法创建对象的时候都是调用的类的公开构造方法,使用反射机制也可以用这种方法来创建对象,所以首先要知道怎么获取构造方法
首先定义的User类有三个构造方法
public User() {
}
private User(String name) {
this.name = name;
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
获取构造方法
@Test
public void test02() throws Exception {
// 获取一个类的结构信息
String path = "com.liu.ref_anno.User";
Class clazz = Class.forName(path);
// 获取全部构造方法
Constructor[] constructors = clazz.getConstructors();
System.out.println(Arrays.toString(constructors));
// 输出结果:[public com.liu.ref_anno.User(), public com.liu.ref_anno.User(java.lang.String,int)]
// 可以找到public的构造方法
// 获取无参数构造的构造方法
Constructor declaredConstructor = clazz.getDeclaredConstructor();
System.out.println(declaredConstructor);
// 输出结果:public com.liu.ref_anno.User()
// 获取有参构造的构造方法
Constructor declaredConstructor1 = clazz.getDeclaredConstructor(String.class, int.class);
System.out.println(declaredConstructor1);
// 输出结果:public com.liu.ref_anno.User(java.lang.String,int)
// 获取private有参的构造方法
Constructor declaredConstructor2 = clazz.getDeclaredConstructor(String.class);
System.out.println(declaredConstructor2);
// 输出结果:private com.liu.ref_anno.User(java.lang.String)
}
2.3.2 获取Class的属性值
/**
* public class User {
* public String name;
* private int age;
* }
*/
@Test
public void test03() throws Exception {
// 获取一个类的结构信息
String path = "com.liu.ref_anno.User";
Class clazz = Class.forName(path);
// 获取所有属性(仅public)
Field[] fields = clazz.getFields();
System.out.println(Arrays.toString(fields));
// 获取对应属性,public
Field name = clazz.getField("name");
System.out.println(name);
// 获取对应属性,可以是private
Field age = clazz.getDeclaredField("age");
System.out.println(age);
/**
* 输出结果
* [public java.lang.String com.liu.ref_anno.User.name]
* public java.lang.String com.liu.ref_anno.User.name
* private int com.liu.ref_anno.User.age
*/
}
2.3.3 获取Class的普通方法
@Test
public void test04() throws Exception {
// 获取一个类的结构信息
String path = "com.liu.ref_anno.User";
Class clazz = Class.forName(path);
// 获取所有方法,仅public
Method[] methods = clazz.getMethods();
System.out.println(Arrays.toString(methods));
// 获取特定方法(无参),仅public
Method getName = clazz.getMethod("getName");
System.out.println(getName);
// 获取特定方法(无参),可private
Method hello = clazz.getDeclaredMethod("hello");
System.out.println(hello);
// 获取特定方法(有参),可private
Method setName = clazz.getDeclaredMethod("setName", String.class);
System.out.println(setName);
}
2.4 操作类中成分
操作类中成分
- 实例化对象
- 操作对象属性
- 操作对象方法
2.4.1 实例化对象
使用反射来实例化对象有两种方法
- 通过Class的newInstance()方法
- 通过Constructor的newInstance()方法
通过Class的newInstance()方法创建对象
- 通过Class的newInstance()方法创建对象
执行newInstance()实际上就是执行无参构造方法来创建该类的实例
@Test
public void test01() throws Exception {
// 获取一个类的结构信息
String path = "com.liu.ref_anno.Dog";
Class clazz = Class.forName(path);
Object o = clazz.newInstance();
System.out.println(o instanceof Dog);
}
- 通过Constructor的newInstance()方法,无参构造
@Test
public void test02() throws Exception {
// 获取一个类的结构信息
String path = "com.liu.ref_anno.Dog";
Class clazz = Class.forName(path);
// 获取无参构造方法
Constructor constructor = clazz.getConstructor();
// 通过无参构造方法来实例化对象
Object o = constructor.newInstance();
System.out.println(o instanceof Dog);
}
- 通过Constructor的newInstance()方法,有参构造
既可以操作public的构造方法,也可以操作private的构造方法
@Test
public void test03() throws Exception {
// 获取一个类的结构信息
String path = "com.liu.ref_anno.Dog";
Class clazz = Class.forName(path);
// 获取有参构造方法,public
Constructor constructor = clazz.getConstructor(String.class, String.class);
// 通过有参构造方法来实例化对象
Object o = constructor.newInstance("旺财", "yellow");
System.out.println(o);
// 获取有参的构造方法,private
Constructor declaredConstructor = clazz.getDeclaredConstructor(String.class);
// private方法要突破封装访问权限的限制
declaredConstructor.setAccessible(true);
// 通过有参构造方法来实例化对象
Object o1 = declaredConstructor.newInstance("大黄");
System.out.println(o1);
}
2.4.2 操作对象属性
使用方法:
- set(Object, Object)
- get(Object)
@Test
public void test04() throws Exception {
// 获取一个类的结构信息
String path = "com.liu.ref_anno.Dog";
Class clazz = Class.forName(path);
Object dog = clazz.getConstructor().newInstance();
// 获取属性
Field name = clazz.getDeclaredField("name");
// 突破访问限制
name.setAccessible(true);
// 设置属性值
name.set(dog, "旺财");
System.out.println(dog);
// 过去属性值
System.out.println(name.get(dog));
}
2.4.3 操作对象方法
使用方法
- invoke(Object obj, Object… args)
@Test
public void test05() throws Exception {
// 获取一个类的结构信息
String path = "com.liu.ref_anno.Dog";
Class clazz = Class.forName(path);
Object o = clazz.getConstructor().newInstance();
// 获取方法
Method setName = clazz.getMethod("setName", String.class);
// 执行方法
setName.invoke(o, "旺财");
System.out.println(o);
}
2.5 使用反射操作泛型
这部分还没看懂,先跳过,今后补充…
三、注解
3.1 注解介绍
注解是从Java5开始引入的一种技术,使用@+名称 就可以对类、方法、参数等内容添加一个描述功能。
在没有出现注解之前,许多技术框架都需要编写大量的XML配置。由于XML文档的结构性问题,想要编写一个文本节点,可能需要2层、3层甚至更多层的嵌套才能实现,但这些问题都可以通过注解来简化。
3.2 常用内置注解
- @Override:在方法重写时,可以在方法上添加@Override注解来检查方法是否满足重写的语法要求。
- @Deprecated:如果在方法上添加这个注解,表示该方法已经过时。过时的方法现在还能使用,但已经不再维护了,不推荐使用
- @SuppressWarning:指示编译器区忽略注解中声明的警告
- @FunctionallInterface:Java8开始支持,标识一个匿名函数或函数式接口
3.3 元注解
看注解的定义中,可以看到每个注解名称上还有几个其他注解。这些注解称为元注解(Meta-annotation)
Java中共有四个元注解:
- @Target
- @Retention
- @Documented
- @Inherited
这四个元注解不是必须同时使用的,一般情况下只需要用到@Target和@Retention,另外两个元注解要根据自己的情况添加。
3.3.1 @Target
@Target
用于指定注解可以使用的位置
注解里面包含了ElementType[]
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* 注解可以用于哪些范围的数组
*/
ElementType[] value();
}
ElementType是一个枚举类型
public enum ElementType {
/** 类、接口、注解、枚举 */
TYPE,
/** 属性值,包括枚举值 */
FIELD,
/** 方法定义 */
METHOD,
/** 参数声明 */
PARAMETER,
/** 构造函数声明 */
CONSTRUCTOR,
/** 局部变量声明 */
LOCAL_VARIABLE,
/** 注解声明 */
ANNOTATION_TYPE,
/** 包声明 */
PACKAGE,
/**
* 类型参数声明
* @since 1.8
*/
TYPE_PARAMETER,
/**
* 类型使用声明
* @since 1.8
*/
TYPE_USE
}
3.3.2 @Retention
@Retention
注解用来标识被注解的元素什么时候起作用
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* RetentionPolicy表示注解的保留策略
*/
RetentionPolicy value();
}
RetentionPolicy是一个枚举值
public enum RetentionPolicy {
/**
* 源文件,注解将被编译器丢弃
*/
SOURCE,
/**
* 字节码,注解在class文件可用,但会被JVM丢弃
*/
CLASS,
/**
* 运行时,常用这个,注解在JVM也会保留,可以通过反射机制读取注解信息
*/
RUNTIME
}
3.3.3 @Documented
生成帮助文档(javadoc)时,是否保留注解信息,如果希望保留就添加这个注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
3.3.4 @Inherited
如果一个注解添加了@Inherited
,那么一个类被这个注解修饰后,其子类也自动添加这个注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
3.4 自定义注解
我们也可以理解元注解进行自定义注解,合理的自定义注解可以简化程序开发。
需求:定义一个注解用来检查属性值中的值大小,注解中传入最大值和最小值,待检查的属性值必须在最大值和最小值之间
1.定义注解
@Retention(value = RetentionPolicy.RUNTIME)
public @interface MyCheck {
int max() default 100;
int min() default 1;
}
2.使用反射实现注解的功能
public class MyCheckImpl {
public static boolean check(Object obj) {
// 1.获取类对象
Class clazz = obj.getClass();
// 2.获取所有属性对象
Field[] fields = clazz.getDeclaredFields();
// 3.遍历每个属性对象
try {
for (Field field : fields) {
// 设置可访问
field.setAccessible(Boolean.TRUE);
// 4.判断属性值是否有MyCheck注解
if (field.isAnnotationPresent(MyCheck.class)) {
// 5.获取注解中的属性值
MyCheck annotation = field.getAnnotation(MyCheck.class);
int max = annotation.max();
int min = annotation.min();
if (min > field.getInt(obj)) {
throw new RuntimeException("当前值为:" + field.getInt(obj) + ", 小于最小值:" + min);
}
if (max < field.getInt(obj)) {
throw new RuntimeException("当前值为:" + field.getInt(obj) + ", 大于最大值:" + max);
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
return false;
}
return true;
}
}
3.使用注解进行测试
public class Dog {
private String name;
private String color;
@MyCheck(max = 100, min = 1)
private int age;
}
public class Demo03 {
public static void main(String[] args) {
Dog dog = new Dog("旺财", "yellow", 101);
// 目前只能使用直接调用的方法进行检查
MyCheckImpl.check(dog);
}
}
/**
* 输出结果
* Exception in thread "main" java.lang.RuntimeException: 当前值为:101, 大于最大值:100
* at com.liu.ref_anno.MyCheckImpl.check(MyCheckImpl.java:26)
* at com.liu.ref_anno.Demo03.main(Demo03.java:6)
*/
四、SpringBoot中自定义注解
在SpringBoot中自定义注解的一般场景为:自定义注解+拦截器或AOP
4.1 自定义注解+拦截器
定义一个注解来判断请求是否需要验证登录
- 定义注解LoginRequired
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
boolean isRequired() default true;
}
- 设置拦截器
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
System.out.println(method.getName() + "=====请求前");
// 检查是否需要验证
if (method.isAnnotationPresent(LoginRequired.class)) {
LoginRequired annotation = method.getAnnotation(LoginRequired.class);
boolean required = annotation.isRequired();
if (required) {
return false;
}
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
System.out.println(method.getName() + "=====请求后");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
System.out.println(method.getName() + "=====所有请求结束后");
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
- 配置拦截器
@Configuration
public class MyInterceptorConfig extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
// 注册我的拦截器
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
super.addInterceptors(registry);
}
}
- 进行测试
@RestController
public class HelloController {
@GetMapping("/method1")
public String method1() {
return "this is source1";
}
@LoginRequired(isRequired = true)
@GetMapping("/method2")
public String method2() {
return "this is source2";
}
}
4.1 自定义注解+AOP
- 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AopLog {
String value() default "自动移注解日志";
}
- 自定义AOP
@Aspect
@Component
public class AopAspect {
// 切入点
@Pointcut("execution(public * com.example.springdemo02.controller.HelloController.*(..))")
public void doPointCut() {}
@Before("doPointCut()")
public void deBefore(JoinPoint joinPoint) throws NoSuchMethodException, ClassNotFoundException {
// 获取对应的类名
String targetName = joinPoint.getTarget().getClass().getName();
// 获取对应的方法名
String methodName = joinPoint.getSignature().getName();
// 获取对应的请求参数
Object[] arguments = joinPoint.getArgs();
// 根据类名获取对应类
Class targetClass = Class.forName(targetName);
// 获取类中对应方法
Method[] methods = targetClass.getMethods();
StringBuilder value = new StringBuilder();
for (Method method : methods) {
// 通过方法名来找到对应方法
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
// 判断参数是否相同
if (clazzs.length == arguments.length) {
// 获取注解对应的日志值
value.append(method.getAnnotation(AopLog.class).value());
break;
}
}
}
System.out.println(value.toString());
}
@AfterReturning(value = "doPointCut()", returning = "returnValue")
public void doAfterReturning(JoinPoint point, Object returnValue){
String name = point.getSignature().getName();
Object[] args = point.getArgs();
System.out.println(name + "后:" + Arrays.toString(args).toString());
}
}
- 定义测试
@RestController
public class HelloController {
@GetMapping("/hello")
@AopLog(value = "Get方法请求/hello")
public String hello(String params) {
return "hello world";
}
}
五、总结
学到这里相信大家已经对反射和注解有了基本的了解,这将会是今后学习框架的基础。
references:
1.https://blog.csdn.net/sco5282/article/details/122075217
2.https://blog.csdn.net/liucy007/article/details/123827715
3.https://blog.csdn.net/zt15732625878/article/details/100061528