反射注解与动态代理

本文将逐一讲解反射、注解、动态代理。之所以将这几个知识点放在一起,是因为三者存在着一定的联系,即反射机制贯穿着注解和动态代理。并且由于在日常实际开发中,虽然很少接触它们的底层,但是对于了解各类框架的底层原理十分有帮助。

反射

什么是反射

在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象的方法的功能称为Java语言的反射机制
反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。

获取 Class 对象的四种方式

如果我们动态获取到这些信息,我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。而Java 提供了四种方式获取 Class 对象:

  • 知道具体类的情况下:
1
Class alunbarClass = TargetObject.class;
  • 通过Class.forName()传入类的全路径:

我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化

1
Class alunbarClass1 = Class.forName("cn.smallboat.TargetObject");
  • 通过对象实例instance.getClass()获取:
1
2
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
  • 通过类加载器xxxClassLoader.loadClass()传入类路径获取:
1
ClassLoader.getSystemClassLoader().loadClass("cn.smallboat.TargetObject");

通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行。

反射的基本操作

  • 创建一个用来反射的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.smallboat;

public class TargetObject {
private String value;

public TargetObject() {
value = "smallboat";
}

public void publicMethod(String s) {
System.out.println("I love " + s);
}

private void privateMethod() {
System.out.println("value is " + value);
}
}
  • 使用反射操作这个类的方法及参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package cn.smallboat;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException {
/**
* 获取 TargetObject 类的 Class 对象并且创建 TargetObject 类实例
*/
Class<?> targetClass = Class.forName("cn.smallboat.TargetObject");
TargetObject targetObject = (TargetObject) targetClass.newInstance();
/**
* 获取 TargetObject 类中定义的所有方法
*/
Method[] methods = targetClass.getDeclaredMethods();
/**
* 输出:publicMethod
privateMethod
*/
for (Method method : methods) {
System.out.println(method.getName());
}

/**
* 获取指定方法并调用
*/
Method publicMethod = targetClass.getDeclaredMethod("publicMethod",
String.class);

// 输出:I love smallboat
publicMethod.invoke(targetObject, "smallboat");

/**
* 获取指定参数并对参数进行修改
*/
Field field = targetClass.getDeclaredField("value");
//为了对类中的参数进行修改我们取消安全检查
field.setAccessible(true);
field.set(targetObject, "smallboat");

/**
* 调用 private 方法
*/
// 输出: value is smallboat
Method privateMethod = targetClass.getDeclaredMethod("privateMethod");
//为了调用private方法我们取消安全检查
privateMethod.setAccessible(true);
privateMethod.invoke(targetObject);
}
}

注解

什么是注解

Java注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

元数据: 元数据是一个非常广泛的概念,元数据的定义也非常简单,只要是描述数据的数据都是元数据。举个例子🌰:如果有一个数字20,我们并不能知道20是什么,但是我们对其加以描述,例如20天20kg重量,那么天数重量这类信息就是描述20这个数据的元数据。

注解是以@注解名在代码中存在的,根据注解参数的个数,我们可以将注解分为:标记注解、单值注解、完整注解三类。它们都不会直接影响到程序的语义,只是作为注解(标识)存在,我们可以通过反射机制编程实现对这些元数据(用来描述数据的数据)的访问

注意: 注解本身是没有任何作用的,比如@RequestMapping,在controller@RequestMapping基本上可以说是必见的,我们都知道他的作用是请求映射,通过他来设置请求的url地址,举例:将@RequestMapping("config")写在方法上,然后我们就可以通过url地址访问到这个方法了,但是记住这并不是@RequestMapping注解的功能,SpringMVC会通过反射将注解当中设置的属性值config拿到,然后将url和你声明注解的方法进行绑定。
所以: 注解只是起标记作用,注解真正的功能都是由框架通过反射来实现的。换句话说,注解离开了反射啥也不是!

注解的分类

注解大致可以根据注解参数(成员变量)个数、注解作用进行分类。

  • 按照注解参数个数可分为:标记注解、单元素注解、多元素注解
  • 按照注解作用可分为:预定义注解、元注解、自定义注解

标记注解

标记注解不包含成员/元素。它仅用于标记声明。
其语法为:@AnnotationName()

由于这些注解不包含元素,因此不需要括号。例如:@Override

单元素注解

单个元素注解仅包含一个元素。
其语法为:@AnnotationName(elementName = "elementValue")

如果只有一个元素,习惯上将该元素命名为value@AnnotationName(value = "elementValue")
在这种情况下,也可以移除元素名称。元素名称默认为value@AnnotationName("elementValue")

多元素注解

这些注解包含多个用逗号分隔的元素。
其语法为:@AnnotationName(element1 = "value1", element2 = "value2")

预定义注解

预定义注解都是java.lang包下的,也就是Java提供的基础注解,直接使用,无需导包。
预定义注解有:

  • @Deprecated :该注解可作用于方法、属性、类,表示已过时,不建议使用,但仍可以使用。
  • @Override:该注解的作用是对覆盖超类中的方法进行标记,如果被标记的方法未被实际覆盖,则编译器会警告。
  • @SuppressWarnings:作用是消除编译器的警告,满足程序员的洁癖。
  • @SafeVarargs:在声明具有模糊类型(比如:泛型)的可变参数的构造函数或方法时,Java编译器会报unchecked警告。鉴于这些情况,如果程序员断定声明的构造函数和方法的主体不会对其varargs参数执行潜在的不安全的操作,可使用@SafeVarargs进行标记。但是只能作用于构造函数和staticfinal修饰的可变长参数方法上。
  • @FunctionalInterface:该注解只能作用域函数接口,标记该接口是一个函数接口,一旦不满足就会报错。

元注解

元注解都是来自java.lang.annotation包下,用于描述注解的属性(元数据描述数据,元注解描述注解),所以这些注解只能修饰注解,但是@Native除外,它是作用于Field(字段)。
1.png

自定义注解

自定义注解主要包含以下几个部分:

  • 元注解
  • public @interface {注解名} {属性}

自定义注解实战:

  • 自定义@Init注解
1
2
3
4
5
6
7
8
@Documented
@Inherited
// 可以在字段、枚举的常量、方法
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Init {
String value() default "";
}
  • 创建User
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class User {
private String name;
private String age;

public String getName() {
return name;
}

@Init("smallboat")
public User setName(String name) {
this.name = name;
return this;
}

public String getAge() {
return age;
}

@Init("24")
public User setAge(String age) {
this.age = age;
return this;
}
}
  • 创建UserFactory工厂(注解实际发挥作用的地方)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.reflect.Method;

public class UserFactory {
public static User create() {
User user = new User();
// 通过反射获取 User 类中所有的方法(getDeclaredMethods也行)
Method[] methods = User.class.getMethods();
try {
for (Method method : methods) {
// 判断方法上是否存在Init注解,存在就返回true,否则返回false
if (method.isAnnotationPresent(Init.class)) {
// 此方法返回该元素的注解在此元素的指定注释类型(如果存在),否则返回null
Init init = method.getAnnotation(Init.class);
// 如果Method代表了一个方法 那么调用它的invoke就相当于执行了它代表的这个方法,在这里就是给set方法赋值
method.invoke(user, init.value());
}
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
return user;
}
}
  • 测试
1
2
3
4
5
6
public static void main(String[] args) {
User user = UserFactory.create();
user.setAge("25");
System.out.println(user.getName());
System.out.println(user.getAge());
}
  • 输出结果
1
2
smallboat
25

动态代理

什么是动态代理

动态代理(Dynamic Proxy) 是一种在运行时动态创建代理对象的机制,它允许你在调用实际对象的方法之前或之后执行额外的逻辑。动态代理通常用于实现横切关注点(cross-cutting concerns)如日志记录、性能监测、事务管理等。
动态代理的工作原理是在运行时创建一个代理类或者代理对象,这个代理对象实现了与被代理对象相同的接口,同时还包含了额外的逻辑。当调用代理对象的方法时,实际上是调用了代理对象的处理器(InvocationHandler)中的逻辑,处理器再决定是否调用被代理对象的方法以及在何时调用。
动态代理的作用在于创建代理对象对原有对象进行功能增强的同时,降低程序的耦合度。
当前实现动态代理的方式有两种:

  • JDK提供的动态代理
  • 第三方提供的CGLIB动态代理

两者的区别

  • JDK 动态代理只能代理实现了接口的类,而 CGLIB 动态代理可以代理没有实现接口的类。
  • JDK 动态代理生成的代理类是目标类的接口的实现类,而 CGLIB 动态代理生成的代理类是目标类的子类。
  • JDK 动态代理不需要额外的依赖,而 CGLIB 动态代理需要单独的依赖。
  • JDK 动态代理性能相对较低,因为使用了反射,而 CGLIB 动态代理性能相对较高,因为使用了字节码技术。

图片来自JavaGuide

  • aop.jpeg

JDK动态代理

JDK 动态代理是 Java 标准库提供的一种动态代理机制,它允许在运行时动态生成代理类和代理对象,用于实现横切关注点(cross-cutting concerns)如日志记录、性能监测、事务管理等。JDK 动态代理主要基于两个核心类:java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler
特点:

  • JDK 动态代理是 Java 标准库的一部分,不需要额外的依赖。
  • 它只能代理实现了接口的类,即目标类必须实现至少一个接口。
  • 使用反射生成代理类和代理对象,代理类和代理对象都是在运行时动态生成的。
  • 代理类和代理对象都是直接实现了被代理接口的,因此不需要继承被代理类。

优点:

  • DK 动态代理是 Java 标准库的一部分,因此不需要额外的依赖。
  • 相对简单易用,适用于代理接口的场景。

缺点:

  • 只能代理接口,无法代理类。
  • 要求目标类必须实现接口,有时会导致代码结构的调整。
  • 性能相对较低,因为代理类和代理对象都是通过反射动态生成的。

代码示例(实现add功能的同时增加日志):

  • 定义接口
1
2
3
interface Calculator {
int add(int a, int b);
}
  • 定义接口实现类
1
2
3
4
5
class CalculatorImpl implements Calculator {
public int add(int a, int b) {
return a + b;
}
}
  • 实现InvocationHandler接口(日志记录拦截器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class LoggingHandler implements InvocationHandler {
private Object target;

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

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在方法调用前记录日志
System.out.println("Before calling method: " + method.getName());

// 调用实际对象的方法
Object result = method.invoke(target, args);

// 在方法调用后记录日志
System.out.println("After calling method: " + method.getName());

return result;
}
}
  • 使用代理处理业务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DynamicProxyExample {
public static void main(String[] args) {
// 创建实际对象
CalculatorImpl calculator = new CalculatorImpl();

// 创建日志记录处理器
LoggingHandler handler = new LoggingHandler(calculator);

// 创建动态代理对象
Calculator proxy = (Calculator) Proxy.newProxyInstance(
Calculator.class.getClassLoader(),
new Class[] { Calculator.class },
handler);

// 使用代理对象调用方法,实际上会触发日志记录
int result = proxy.add(5, 3);
System.out.println("Result: " + result);
}
}

CGLIB动态代理

CGLIB 是一个强大的、高性能的代码生成库,它用于在运行时生成字节码,通过扩展被代理类来创建代理对象。与 Java 标准库中的动态代理不同,CGLIB 可以代理那些没有实现接口的类,并且不需要目标对象实现任何接口。
特点:

  • CGLIB 是一个独立的第三方库,需要单独的依赖。
  • 它能够代理没有实现接口的类,即目标类可以是普通的类,而不需要实现接口。
  • 使用字节码技术生成代理类,代理类是目标类的子类,因此可以代理 final 类和方法。
  • 生成的代理对象是目标类的子类,因此需要继承目标类。

优点:

  • 能够代理没有实现接口的类,更加灵活。
  • 性能相对较高,因为代理类是目标类的子类,不需要使用反射。

缺点:

  • 需要额外的依赖,不是 Java 标准库的一部分。
  • 生成的代理类是目标类的子类,可能会有类加载器和字节码兼容性等问题。
  • 无法代理 final 类和方法以及 static 方法。

代码示例(实现add功能的同时增加日志):

  • 实际实现类(注意这次不需要实现接口)
1
2
3
4
5
class Calculator {
public int add(int a, int b) {
return a + b;
}
}
  • 实现InvocationHandler接口(日志记录拦截器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LoggingInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 在方法调用前记录日志
System.out.println("Before calling method: " + method.getName());

// 调用实际对象的方法
Object result = proxy.invokeSuper(obj, args);

// 在方法调用后记录日志
System.out.println("After calling method: " + method.getName());

return result;
}
}
  • 使用代理处理业务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CglibProxyExample {
public static void main(String[] args) {
// 创建 Enhancer 对象,用于生成代理类
Enhancer enhancer = new Enhancer();
// 设置被代理类
enhancer.setSuperclass(Calculator.class);
// 设置拦截器
enhancer.setCallback(new LoggingInterceptor());

// 创建代理对象
Calculator proxy = (Calculator) enhancer.create();

// 使用代理对象调用方法,实际上会触发日志记录
int result = proxy.add(5, 3);
System.out.println("Result: " + result);
}
}