Java的反射与代理

Soul Lv2

最近比较闲,所以来整点活,让我们从0到1手写一个Spring框架吧(只使用Java SE部分的相关内容,不使用任何依赖),不过在正式开始之前,我们要去了解一些基础知识(如果你确信了解了这些知识,可以直接跳过)。

我们今天来讲一讲反射与代理相关的知识。

首先我们从Java的类加载机制讲起

Java的类加载机制

我们都知道对于Java来说万物皆对象,所以你猜猜类是什么?类当然是对象了,所有的类在JVM中都被视为一种特殊的对象:Class对象。我们先反编译出来看一看,下面是这个类的头部

1
public final class Class<T> implements Serializable, GenericDeclaration, Type, AnnotatedElement, TypeDescriptor.OfField<Class<?>>, Constable

类的加载流程大致如下

  1. JVM读取.class文件并将字节码转化为二进制数据,依照二进制数据创建一个上面提到的java.lang.Class对象来表示某个特定的类
  2. 进行验证,确保所有的Class对象符合规范,不会危害JVM安全
  3. 开始为对应的静态属性分配内存并设置默认的初始值
  4. 进行解析,为所有的属性,方法添加对应的引用映射,如将类名指向Class对象
  5. 执行类加载操作,调用对应的类加载器进行类的加载

我们可以在Class类中发现Class类的构造方法

1
2
3
4
private Class(ClassLoader loader, Class<?> arrayComponentType) {
this.classLoader = loader;
this.componentType = arrayComponentType;
}

其中的ClassLoader就是我们所说的类加载器

Java 的类加载器采用层次结构,主要包括以下几种:

  1. 启动类加载器(Bootstrap ClassLoader)
    • 负责加载 Java 核心类库(如 rt.jar 中的类)。
    • 它是虚拟机的一部分,通常由 C++ 实现,不是 Java 代码。
    • 没有父类加载器。
  2. 扩展类加载器(Extension ClassLoader)
    • 负责加载 Java 的扩展类库(如 jre/lib/ext 目录下的 JAR 文件)。
    • 它是 sun.misc.Launcher$ExtClassLoader 的实例,父类加载器是启动类加载器。
  3. 应用程序类加载器(Application ClassLoader)
    • 也称为系统类加载器,负责加载用户类路径(CLASSPATH)指定的类。
    • 它是 sun.misc.Launcher$AppClassLoader 的实例,父类加载器是扩展类加载器。
  4. 自定义类加载器
    • 用户可以通过继承 java.lang.ClassLoader 类来实现自定义类加载器。
    • 用于加载特定路径或来源的类。

我们先来看一看这个默认的类加载器在干什么吧,由于私有方法太多,我这里只贴一些比较关键的内容,首先是一个最为重要的类加载方法

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
public Class<?> loadClass(String name) throws ClassNotFoundException {
return this.loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(name)) {
Class<?> c = this.findLoadedClass(name);//这个方法会调用一个通过C++实现方法检测类是否已经被加载
if (c == null) {//如果没有加载则开始加载
long t0 = System.nanoTime();//此处为获取JVM运行时间
try {
if (this.parent != null) {
c = this.parent.loadClass(name, false);//如果该加载器存在父类则调用父加载器进行加载
} else {
c = findBootstrapClassOrNull(name);//如果不存在则进行加载 ,这个方法同样是C++实现的
}
} catch (ClassNotFoundException var10) {//如果父加载器加载失败则捕获异常并继续运行
}
if (c == null) {
long t1 = System.nanoTime();
c = this.findClass(name);//这个方法现在实际上是一个空方法,交给子类实现,默认运行到此处时会直接抛异常
PerfCounter.getParentDelegationTime().addTime(t1 - t0);//这些都是用于记录类加载时间的,用于性能优化
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
this.resolveClass(c);//这里是永远不会对外界开放的,不必关心
}
return c;
}
}

你会发现这样一个逻辑:一般情况下,总是优先调用父加载器进行加载,这种加载机制被称之为双亲委派机制,原生的所有类加载器都会尽可能的将类加载任务委派给自己的父类的实例,这样做的根本目的是避免一个类被重复加载。

还有一个比较关键的类加载器方法

1
2
3
4
5
6
7
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError 
protectionDomain = this.preDefineClass(name, protectionDomain);
String source = this.defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source);
this.postDefineClass(c, protectionDomain);
return c;
}

这个方法用来将二进制数据转换为类对象,不过很遗憾,这个方法也是C++实现的

那么这个类加载器在哪里被调用呢,可以简单的认为当某个类第一次被需要时,JVM会尝试调用类加载器来完成加载,这一部分我们在讲解反射部分时会具体的展示。

我们最后再梳理以下JAVA的类加载机制:当某个类被需要时,JVM将通过类加载器直接加载这个类,这一过程遵循双亲委派机制,加载的结果是一个Class对象

反射操作

接下来我们解释什么是反射。这是我们一般情况下获取对象的方法

1
Test test =new Test()

不管怎么改,无论是什么建造者模式,工厂模式等等各种花活,都逃不出在某处new一个对象出来,但是你注意到这样一个问题了吗?Spring的依赖注入是如何实现的,框架的编写者是无法事先得知对象的名称的,又如何获取对象用来注入呢?正是通过反射

举一个简单的例子,我可以通过这样的方式来构建一个字符串对象

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
try {
String aString =(String) String.class.getConstructor().newInstance();
String bString = (String) Class.forName("java.lang.String").getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

你看,我全程绝对没有new,哪怕你顺着源码查下去也绝对找不到任何new,这就是创建对象的另一种方式:反射。你或许想不到这有什么用,但是不要着急,我们先来了解以下关于反射的各种使用

构建对象

在知道对象名称时我们可以这样来实现

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
try {
Class<?> stringClass = Class.forName("java.lang.String");
Constructor<?> stringConstructor = stringClass.getConstructor();//获得类的构造器
Object string = stringConstructor.newInstance();//使用构造器来完成构造
} catch (Exception e) {
throw new RuntimeException(e);
}
}

上面的是JDK9之后的写法,如果你使用JDK8,甚至可以直接

1
2
3
4
5
6
7
8
public static void main(String[] args) {
try {
Class<?> stringClass = Class.forName("java.lang.String");
Object string = stringClass.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

我们上面还提到了

1
Class<?> intClass =int.class;

这样的方法来获取类对象, class属性是直接来自于Object对象的,所有对象都天然的拥有这个属性。

当然,对于一个对象,你也可以通过

1
2
String s="Hello World";
Class<?> c =s.getClass();

这样的方式来获取class对象,在获取类对象后你可以按照上面的流程来获取对象。

请注意

  • 所有的基本数据类型,基本数据类型的数组类型,基本数据类型的包装类型都拥有独立且唯一的Class对象
  • 类本身和对应的数组类型是拥有独立的Class对象
  • 所有的类永远只拥有一个类对象(这一点在必要时是可以被手动打破的)

值得一提的是在利用类对象获取构造器时我们可以主动的选择构造方法,例如对于这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student(){
this.age = 0;
this.name = "0";
}
public String test(){
return name;
}
}

我们可以做一个这样的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("Student");
Constructor<?> constructor1 = clazz.getConstructor();//获取无参构造
Constructor<?> constructor2 = clazz.getConstructor(String.class,int.class);//获取全参构造
Student student1 = (Student) constructor1.newInstance();
Student student2 = (Student) constructor2.newInstance("aaa",111);
System.out.println(student1.test());
System.out.println(student2.test());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

获取对象信息

我们可以通过反射来获取一些类的信息

1
2
3
4
5
6
7
8
Class<?> clazz = Class.forName("Student");
System.out.println(clazz.getSuperclass().getName());//获取父类
for(Class<?> c: clazz.getInterfaces()){
System.out.println(clazz.getName());//获取接口
}
for (Annotation annotation: clazz.getAnnotations()){
System.out.println(annotation.annotationType().getName());
}//获取注解

此外,反射还能允许我们做一些比较疯狂的事情,比如访问私有字段,还是上面的Student类,age字段显然是私有的,且不存在任何方法进行访问,真的吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
try {
Student student = new Student();
Field field = student.getClass().getDeclaredField("age");//获取字段
field.setAccessible(true);//强制运行字段访问
System.out.println(field.get(student));//输出0
field.setInt(student, 11);//修改字段值
System.out.println(field.get(student));//输出11
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

实际上哪怕是final字段。只要愿意也是可以修改的(不过JDK9之后禁止了,但仍然可以通过一些方式开启)

这也意味这个一个问题:Java的一个核心理念:封装被突破了。你不让我访问?这是你能挡得住的?我不但能访问,我还能修改。所以实际上所有的封装实际上来自于程序员之间的君子约定:我们互相约定好这部分只在规定范围内访问,当然如果你非要访问,也没什么办法。

调用方法

我们甚至都已经访问了属性了,怎么能做不到调用方法呢?为了方便解释我们先改一改Student方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student(){
this.age = 0;
this.name = "0";
}
public String test(){
return name;
}
public String test(String s){
return s+name;
}
}

然后直接上

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
try {
Student student = new Student();
Method method = student.getClass().getMethod("test",String.class);//通过名称与参数类型来获取方法
System.out.println(method.invoke(student,"1"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

由于编译时形参无法被保留,我们无法直接通过名称获得

特别的,如果参数类型为可变参数,可选择

1
Method method = clazz.getDeclaredMethod("test", String[].class);

不过对于私有方法,就像final字段一样,默认情况下无法被直接使用,需要在启动时添加

1
--add-opens java.base/java.lang=ALL-UNNAMED

来允许所有反射

实现一个简单的IoC容器

有了上面的基础,我们已经可以实现一个简单的Bean管理器了。我们先回忆以下Spring中Bean管理器拥有哪些功能:

  1. 扫面并创建所有的Bean
  2. 完成依赖注入
  3. 支持在任何地方提供Bean

我们逐个来完成这些需求,为了方便,我们这里只提供注解形式的注册Bean的方法,先写一个注解

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Componenet {
}

还要写一个用于自动绑定的注解

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AutoWired {
}

我们尝试搭建一个大概的框架

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
public class MyIocContainer {
private Map<String, Object> beanMap = new HashMap<>();
private List<String> basePackageList =new ArrayList<>();
private final ClassPathLoader classPathLoader = new ClassPathLoader();

public void addBasePackage(String basePackage) {
this.basePackageList.add(basePackage);
}//用于规定扫描路径

public void scanPackage(Class<?> clazz) {//此处模仿Spring,填入Main.class来确定运行位置
if(basePackageList.isEmpty()) {
basePackageList.add(clazz.getPackage().getName());
}
for (String basePackage : basePackageList) {//遍历所有路径加载Bean
try {
loadBean(beanMap, basePackage);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
autoWiring();
}

public Object getBean(String className) {
return beanMap.get(className);
}
private void autoWiring(){
for (Map.Entry<String, Object> entry : beanMap.entrySet()) {
for (Field field : entry.getValue().getClass().getDeclaredFields()) {//遍历所有的Bean完成依赖注入
field.setAccessible(true);
if(!field.isAnnotationPresent(AutoWired.class)){
continue;
}
if(beanMap.containsKey(field.getType().getName())){
try {
field.set(entry.getValue(),beanMap.get(field.getType().getName()));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}else {
throw new RuntimeException("只能自动绑定已注册的Bean");
}
}
}
}
}

接下来完成加载Bean的方法

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
private void loadBean(Map<String,Object> beanMap, String packageName) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
URL dir = classLoader.getResource(packageName);//这部分文件的获取实际上使用类路径,这个概念我们下一篇再来研究
packageName = packageName.replace('/', '.');
if (dir == null) {
return;
}//如果投入的路径不存在,直接返回
File dirFile = new File(dir.getFile());
if (!dirFile.exists() || !dirFile.isDirectory()) {
return;
}//对应的类路径不存在或者不是目录,直接返回
File[] files = dirFile.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
loadBean(beanMap, packageName.replace(".","/") + "/" + file.getName());
continue;
}
String className = packageName + "." + file.getName().replace(".class", "");
Class<?> clazz = classLoader.loadClass(className);
if(clazz.isInterface() || clazz.isAnnotation() || clazz.isEnum() || Modifier.isAbstract(clazz.getModifiers())) {
continue;
}//接口,抽象类和注解都不能使用构造方法构造,直接跳过避免报错
if(clazz.isAnnotationPresent(Componenet.class)) {
Constructor<?> constructor =clazz.getConstructor();
constructor.setAccessible(true);
beanMap.put(className,constructor.newInstance());
}//加载类
}
}
}

虽然非常的简陋,但是这个IoC容器在功能上已经和Spring的IoC容器一致了,我们的全部实现只有不到100行代码。不过,这个容器仍然存在很多问题,举个简单的例子,如果有些类中包含一些static属性,这些属性对应的类在加载当前类时并没有被正确的加载,那么报错是必然的,想一想该怎么处理这个问题。此外,如果类足够多,我们是不是需要添加对应的多线程支持,这个类显然没有考虑多线程的情况,该怎么解决?这些问题我先留在,我们将来再解决。

动态代理是什么

所谓的动态代理就是通过反射获取类的相关方法,在执行时代替这个类去执行对应的方法,在这一过程中可以对方法进行一定的修饰

所以这有什么用?仔细想想,Mybatis是怎么实现的?不就是通过对接口方法的代理吗?Spring的AOP或者说面向切面怎么实现的,不也是通过对类的代理吗?

如果你想的话可以直接自己手搓一个动态代理机制出来,不过Java毕竟原生提供了动态代理的实现,我们还是直接调用吧,其中的关键是两个类

  • java.lang.reflect.Proxy:代理类,用于动态创建代理对象。
  • java.lang.reflect.InvocationHandler:调用句柄接口,用于处理代理对象的方法调用。

我们写一个简单的例子,这是一个接口

1
2
3
4
public interface Service {
void doSomething();
String test();
}

接着完成这个接口的实现

1
2
3
4
5
6
7
8
public class ServiceImpl implements Service {
public void doSomething() {
System.out.println("doSomething");
}
public String test() {
return "test";
}
}

接下来我们完成一个代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyProxy implements InvocationHandler {
private final Object target;
public MyProxy(Object target) {
this.target = target;
}

@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
System.out.println("执行前操作");
Object result = method.invoke(target, objects);
System.out.println("执行后操作");
return result;
}
public static Object getProxyInstance(Object target) {
return Proxy.newProxyInstance(target.getClass().getClassLoader(),//获得类加载器
target.getClass().getInterfaces(),//获得所实现的接口
new MyProxy(target));//提供一个调用句柄
}
}

你会发现这个代理类是通用的,接下来我们尝试使用以下代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) {
Service service1 = new ServiceImpl();
Service service2 = (Service) MyProxy.getProxyInstance(service1);

service1.doSomething();
service2.doSomething();

System.out.println(service1.test());
System.out.println(service2.test());

System.out.println(service1.hashCode());
System.out.println(service2.hashCode());

System.out.println(service1.getClass().getName());
System.out.println(service2.getClass().getName());
}
}

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
执行前操作
doSomething
执行后操作
test
执行前操作
执行后操作
test
2084435065
执行前操作
执行后操作
2084435065
abc.proxy.ServiceImpl
jdk.proxy1.$Proxy0

在我不解释的情况下你是否能理解为什么会出现这样的输出结果呢,我们分段来看

首先是前四行输出,无代理的对象直接输出,有代理的对象按照我们所规定的操作顺序来处理

接着看5到8行,执行顺序变了吗?并没有,只不过在受到代理的对象中,调用任何一个方法本质上都是通过我们重写的invoke方法来实现的,控制台输出在Invoke方法执行完毕后才得到了要输出的结果

再来看一看这个8到11行,不是说会产生新的代理类吗,那么为什么哈希是相同的,有点诡异。

再看一看12和13行,类名不一样,是符合我们预期的,不过还有一个问题,这次从结果上看居然没有被代理?这是什么情况?原因实际上非常简单,如果粗暴的将所有的方法代理,那么区分代理类与原来的类就成了一件麻烦的事情,所以这个方法并没有被代理。

实现一个简单的AOP框架

我们可以使用动态代理来实现一个简单的AOP框架,不过很遗憾我们现有的知识是不足以让我们完整的实现Spring的AOP能力的,JDK的动态代理只做到了类一级,不能精细的对方法进行控制,这种能力需要对特定的字节码进行操作,所以我们还是暂时不讲了。

不过简单的类级别的AOP框架还是很容易实现的,我们试着写一个吧,为了方便,我们这里只考虑三种情况:执行前,执行后,抛出异常时。我们将这三种情况规范为对应的接口

1
2
3
4
5
6
7
8
9
10
11
public interface BeforeExecution {
void before(Object[] objects) throws Throwable;
}

public interface BeforeExecution {
void before(Object[] objects) throws Throwable;
}

public interface ThrowException {
void throwException(Object[] objects,Throwable throwable) throws Throwable;
}//这里的objects实际上是方法的参数,使用者可以利用这些参数

然后考虑创建对应的代理类,我随便写一个可能的例子

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
53
54
55
56
57
58
59
60
61
62
63
public class ProxyFactory  {
private static final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
private static AfterExecution afterExecution = null;
private static ThrowException throwException = null;
private static BeforeExecution beforeExecution = null;
private static boolean isClean=true;

public static void setAfterExecution(AfterExecution afterExecution) {
ProxyFactory.afterExecution = afterExecution;
}

public static void setThrowException(ThrowException throwException) {
ProxyFactory.throwException = throwException;
}

public static void setBeforeExecution(BeforeExecution beforeExecution) {
ProxyFactory.beforeExecution = beforeExecution;
}

private static Object getProxy0(Object target,InvocationHandler invocationHandler) throws Throwable {
return Proxy.newProxyInstance(classLoader, target.getClass().getInterfaces(), invocationHandler);
}

public static Object getProxy(Object target) throws Throwable {
if(!isClean) {
throw new RuntimeException("使用后未复位");
}else{
isClean=false;
}
//这里其实采用的是装饰模式的思想,不断的对原来的类进行装饰最终得到我们想要的对象
if (beforeExecution != null) {
target = getProxy0(target, (o, method, objects) -> {
beforeExecution.before(objects);
return method.invoke(o, objects);
});
}
if (afterExecution != null) {
target =getProxy0(target,(o,method,objects)->{
Object result = method.invoke(o, objects);
afterExecution.afterExecution(objects);
return result;
});
}
if (throwException != null) {
target =getProxy0(target,(o,method,objects)->{
Object result = null;
try {
result = method.invoke(o, objects);
}catch (Throwable throwable) {
throwException.throwException(objects ,throwable);
}
return result;
});
}
return target;
}
public static void clean() {//这是一个复位方法,避免产生不必要的代理
isClean=true;
afterExecution=null;
throwException=null;
beforeExecution=null;
}
}

结语

这部分的知识就讲到这里,我们下一篇将会探讨关于文件处理相关的问题,顺路继续优化我们的IoC容器

1743348809308

  • 标题: Java的反射与代理
  • 作者: Soul
  • 创建于 : 2025-04-04 14:39:10
  • 更新于 : 2025-04-07 17:42:12
  • 链接: https://soulmate.org.cn/posts/fabcc437/
  • 版权声明: 本文章采用 CC BY-NC 4.0 进行许可。