class类
除了int double等基本类型之外 java所有的其他类型全部都是class 包括interface 例如:
StringObjectRunnableException
在java当中 无继承关系的数据类型无法赋值:
1 | Number n = new Double(123.456); // OK |
在java当中 当我们写好了.java文件过后 编译时就会发生以下操作 会在硬盘上保存对应的.class文件一个类对应一个.class文件 比如我这里文件里面有一个类叫做test 那么同样在硬盘里面生成一个test.class的文件 当然 内置类不参与这个过程
然后当程序需要第一次调用到哪个类时就会去硬盘上读取对应的.class文件 读取到内存当中去 在后面需要调用该类时就直接在内存里面用了
而反射就是反过来的过程
我们有三种方法能够获取到在Class实例
第一种:
1 | Class cls = Text.class; |
这种方法是我们已知该类的名字 直接跟上class后缀 左边的Class代表数据类型 也就是说这里的cls拥有了Text类的所有性质
第二种:
这种方法就是我们已经具有实例变量了 我们在之前已经new了一个实例 那我们就可以调用getClass()去获取Class
1 | Text t=new Text(); |
第三种:
如果知道一个class的完整类名,可以通过静态方法Class.forName()获取,在forName()当中需要填入的是完整的包名加类名:
1 | package com.ctf.utils; |
这里使用forName()获取就是:
1 | Class cls=Class.forName(com.ctf.util.HackerTools); |
因为反射的目的是为了获得某个实例的信息。因此,当我们拿到某个Object实例时,我们可以通过反射获取该Object的class信息:
1 | void printObjectInfo(Object obj) { |
要从Class实例获取获取的基本信息,参考下面的代码:
1 | // reflection |
注意到数组(例如String[])也是一种类,而且不同于String.class,它的类名是[Ljava.lang.String;。此外,JVM为每一种基本类型如int也创建了Class实例,通过int.class访问。
如果获取到了一个Class实例,我们就可以通过该Class实例来创建对应类型的实例:
1 | // 获取String的Class实例: |
上述代码相当于new String()。通过Class.newInstance()可以创建类实例,它的局限是:只能调用public的无参数构造方法。带参数的构造方法,或者非public的构造方法都无法通过Class.newInstance()被调用。
其实newInstance()和new的作用都是实例化 在这个前面加上一个(String)计时要求JVM返回的必须是String类型的
访问字段
对任意的一个Object实例,只要我们获取了它的Class,就可以获取它的一切信息。
我们先看看如何通过Class实例获取字段信息,字段其实我们可以理解为我们所常说变量就好
Class类提供了以下几个方法来获取字段:
- Field getField(name):根据字段名获取某个public的field(包括父类)这个方法只能拿到public的字段
- Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)这个方法就是不管public还是private都可以拿了 但是这个只能拿到当前类的 并不会去到父类里面寻找
- Field[] getFields():获取所有public的field(包括父类)把自己和父类的所有public全部拿到并且存到数组里面
- Field[] getDeclaredFields():获取当前类的所有field(不包括父类)把自己的所有变量全部拿到并且存到数组里面
下面就一一来举例子:
1 | // 例子:成功拿到当前类的公开变量 score |
1 | // 例子:成功拿到当前类私有的变量 school |
1 | // 例子:拿到一个数组,里面装了 score 和 name(因为这两个是公开的) |
1 | // 例子:拿到一个数组,里面装了 score 和 school(因为只有这两个写在当前类里) |
下面来看一段完整的示范代码:
1 | // reflection |
上述就是先把Student的Class拿到然后去获取字段,输出会类似于:
1 | public int Student.score |
一个Field对象包含了一个字段的所有信息:
getName():返回字段名称,例如,"name";getType():返回字段类型,也是一个Class实例,例如,String.class;getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的含义。
第一个getName肯定就不说了 第二个getType就是返回类型吗 String我们知道是一个类所以返回的是java.lang.String
第三个需要说一下 它虽然作用是获取修饰符比如public private final这种 但是传出来的值其实是数字:
| 修饰符 | 控制第几盏灯(二进制位) | 对应的十进制数字 | 大白话解释 |
|---|---|---|---|
public |
0000 0000 0000 0001 (第 1 位) |
1 |
公开开关 |
private |
0000 0000 0000 0010 (第 2 位) |
2 |
私有开关 |
protected |
0000 0000 0000 0100 (第 3 位) |
4 |
受保护开关 |
static |
0000 0000 0000 1000 (第 4 位) |
8 |
静态开关 |
final |
0000 0000 0001 0000 (第 5 位) |
16 |
只读(不可变)开关 |
例子 A:public static 变量
- 变量定义:
public static int score; - JVM 检查发现:
public灯亮了(1),static灯亮了(8)。 - 最终返回的数字:$1 + 8 =$
9。 - 二进制状态:
0000 0000 0000 1001
例子 B:private static final 变量
- 变量定义:
private static final String token; - JVM 检查发现:
private灯亮了(2),static灯亮了(8),final灯亮了(16)。 - 最终返回的数字:$2 + 8 + 16 =$
26。 - 二进制状态:
0000 0000 0001 1010
获取字段值
获取字段值其实就是主要依靠get() 有时无法访问到我们就尝试用setAccessible(true)
利用反射拿到字段的一个Field实例只是第一步,我们还可以拿到一个实例对应的该字段的值。
例如,对于一个Person实例,我们可以先拿到name字段对应的Field,再获取这个实例的name字段的值:
1 | // reflection |
上述代码先获取Class实例,再获取Field实例,然后,用Field.get(Object)获取指定实例的指定字段的值。
运行代码,如果不出意外,会得到一个IllegalAccessException,这是因为name被定义为一个private字段,正常情况下,Main类无法访问Person类的private字段。要修复错误,可以将private改为public,或者,在调用Object value = f.get(p);前,先写一句:
1 | f.setAccessible(true); |
调用Field.setAccessible(true)的意思是,别管这个字段是不是public,一律允许访问。
设置字段值
设置字段值是通过Field.set(Object, Object)实现的,其中第一个Object参数是指定的实例,第二个Object参数是待修改的值。示例代码如下:
1 | // reflection |
运行上述代码,打印的name字段从Xiao Ming变成了Xiao Hong,说明通过反射可以直接修改字段的值。
同样的,修改非public字段,需要首先调用setAccessible(true)。
这里其实就是拿f来当指针的感觉 f来拿到name然后进行操作
调用方法
Class类提供了以下几个方法来获取Method:
Method getMethod(name, Class...):获取某个public的Method(包括父类)Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)Method[] getMethods():获取所有public的Method(包括父类)Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
其实这个和我们上面提到的获取字段值是一个道理的
下面进行举例 我们进行前提假设:
1 | // 父类:人 |
① getMethod(name, Class...)
大白话: 凭名字和参数类型,只找公开的方法(包括继承自爸爸的方法)。
Java
1 | // 例子:精准拿到子类 Student 身上,名字叫 "study"、且参数是 int 的公开方法 |
② getDeclaredMethod(name, Class...)
大白话: 凭名字和参数类型,只在当前子类里找(私有的也能拿,但绝对不看爸爸的)。
Java
1 | // 例子:精准拿到子类 Student 内部,名字叫 "cheat"、且没有参数的私有方法 |
③ getMethods()
大白话: 别管名字,把子类自己和父类身上所有【公开】的方法全部打包送过来。
Java
1 | // 例子:拿到一个 Method 数组,里面装了 study、eat 以及 Java 自带的其它公开方法(如 toString) |
④ getDeclaredMethods()
大白话: 别管名字,把【当前子类】内部写的所有方法全抱过来,不管公开还是私有(但不要爸爸的)。
Java
1 | // 例子:拿到一个 Method 数组,里面硬生生只装了 study 和 cheat 这两个在 Student 类里手写的方法 |
如果出现多参数的情况那我们就需要严格按照参数的顺序写入我们的getMethod():
- 前提:准备一个多参数的方法
我们在 Student 类里手写一个需要传入三个参数的公开方法 register(注册):
Java
1 | class Student { |
- 完整的一行句子例子
现在我们要精准把这个带有三个参数的 register 方法探测器给揪出来:
Java
1 | // 例子:名字后面跟着三个 Class 实例,顺序必须和方法定义的完全一致(String -> int -> boolean) |
上述代码首先获取Student的Class实例,然后,分别获取public方法、继承的public方法以及private方法,打印出的Method类似:
1 | public int Student.getScore(java.lang.String) |
一个Method对象包含一个方法的所有信息:
getName():返回方法名称,例如:"getScore";getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class;getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class};getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。
同样这里的getModifiers是一个数字 但是原理就和上面的字段部分的内容是一样的了
调用方法
当我们获取到一个Method对象时,就可以对它进行调用。我们以下面的代码为例:
1 | String s = "Hello world"; |
如果用反射来调用substring方法,需要以下代码:
1 | // reflection |
注意到substring()有两个重载方法,我们获取的是String substring(int)这个方法。思考一下如何获取String substring(int, int)方法。
对Method实例调用invoke就相当于调用该方法,invoke的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。
调用静态方法
如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke方法传入的第一个参数永远为null。我们以Integer.parseInt(String)为例:
1 | // reflection |
调用非public方法
和Field类似,对于非public方法,我们虽然可以通过Class.getDeclaredMethod()获取该方法实例,但直接对其调用将得到一个IllegalAccessException。为了调用非public方法,我们通过Method.setAccessible(true)允许其调用:
1 | // reflection |
此外,setAccessible(true)可能会失败。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对java和javax开头的package的类调用setAccessible(true),这样可以保证JVM核心库的安全。
多态
我们来考察这样一种情况:一个Person类定义了hello()方法,并且它的子类Student也覆写了hello()方法,那么,从Person.class获取的Method,作用于Student实例时,调用的方法到底是哪个?
1 | // reflection |
运行上述代码,发现打印出的是Student:hello,因此,使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。上述的反射代码:
1 | Method m = Person.class.getMethod("hello"); |
实际上相当于:
1 | Person p = new Student(); |
调用构造方法
其实本质上从流程和原理来说和字段以及方法的内容仍然是一致的
我们知道在反射当中可以利用newInstance()来进行获取实例 但是如果单使用newInstance()的情况下 我们是无法进行直接去调用带参数或者非public的构造方法的 我们如果想要调用就需要借助到constructor这个概念
本质还是一样的 先把构造方法拿出来 然后再用newInstance()去调用该方法:
比如有如下构造方法 我们可以看见这里需要传参的
1 | class Text { |
如果我们需要在反射当中调用该构造方法的话 那我们就需要:
1 | public class Main{ |
过Class实例获取Constructor的方法如下:
getConstructor(Class...):获取某个public的Constructor;getDeclaredConstructor(Class...):获取某个Constructor;getConstructors():获取所有public的Constructor;getDeclaredConstructors():获取所有Constructor。
注意Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
调用非public的Constructor时,必须首先通过setAccessible(true)设置允许访问。setAccessible(true)可能会失败。
获取继承关系
获取父类
这个就比较简单 直接用getSuperclass()就可以了 但是只会往上找一次
1 | // 顶层父类 |
输出:
1 | Dog的直接父类:class Animal |
获取interface
同上 用getInterfaces()就可以了
但是需要注意的是获取接口只会得到当前类的接口 不会去获取父类的接口
1 | // reflection |
获取继承关系
当我们判断一个实例是否是某个类型时,正常情况下,使用instanceof操作符,instanceof()是一个拿来进行判断而非上面所提到那些一样是用来获取的:
1 | Object n = Integer.valueOf(123); |
再举一个常规一点的例子:
1 | // 父类:动物 |
我们之前学过关于转型的知识 子类可以往父类上转型 但是父类不能向子类转型 比如我们可以把一个int类型的赋值给一个number类型的 但是我们不能把一个number类型的赋值给一个int类型的 因为向下转型是不被允许的
如果是两个Class实例,要判断一个向上转型是否成立,可以调用isAssignableFrom():
1 | // Integer i = ? |
简化一下语法就是:
1 | A.class.isAssignableFrom(B.class) |
也就是判断 B可不可以转型过后赋值给A
静态代理
核心定义:为目标对象提供一个代理对象,并由代理对象控制对目标对象的访问。代理对象可以在目标对象方法执行的前后,进行额外的增强处理,目标对象只需要专注于自己的核心业务逻辑即可。
生活中的通俗例子:
你想租房,不用自己挨个找房东看房、签合同,只需要找「房产中介」,中介就是你的代理,帮你处理租房的杂事,你只需要最终签约入住即可。
明星开演唱会,不用自己对接场地、票务、安保,只需要找「经纪人」,经纪人就是明星的代理,明星只需要专注于唱歌表演。
在这个逻辑里:你 / 明星 = 目标对象,中介 / 经纪人 = 代理对象,核心业务是「租房入住」「唱歌表演」,额外的增强逻辑是「找房源、签合同」「对接场地、处理票务」。
代理模式的核心价值总结:
✅ 解耦:目标对象和增强逻辑彻底分离,互不影响
✅ 增强:在不修改目标对象源码的前提下,对方法功能做扩展
✅ 复用:增强逻辑可以被多个目标对象复用,不用重复编写
静态代理的特点就是代理类是程序员手动编写的 编译器就确定好的类 一个目标类对应一个代理类 在代码编译之前,我们手动专门写一个「代理类」;这个代理类和被代理的目标类实现同一个接口,在不修改原有业务代码的前提下,给目标方法额外增加前置、后置附加逻辑(日志、参数校验、耗时统计、权限判断等)。
下面我们来看例子:
我们用一个经典的「业务功能 + 日志记录」案例实现,需求:在执行业务方法的执行前打印请求日志、执行后打印响应日志,且不修改业务类源码。
步骤一:定义核心业务接口:
1 | /** |
步骤二:实现目标类(真实目标类)
1 | /** |
步骤三:编写静态代理类
我们的增强都是在这里完成的
1 | /** |
1 | private UserService userService; |
这里就是定义一个private UserService类型的变量userService
我们后面又可以看到
1 | public UserServiceProxy(UserService userService) { |
其实这里就是为了我们可以根据不同的需求去传不同的参数 就比如我们想要用的就是那个UserServiceImpl类 那我们就传它 那如果我们想要实现的是另外一个类那我们就穿它就行了 包括我们下面会看到类似:
1 | public void addUser() { |
当中的:
1 | userService.addUser(); |
之所以会这样写也是为了调用对应类里面的方法
步骤四:测试调用
1 | public class StaticProxyTest { |
在这里我们会看到:
1 | UserService userService = new UserServiceImpl(); |
这样写是合法的是由于之前学习到的向上转型的相关知识 一个类如果去实现一个接口的话 那么这个接口就是我们所说的上层了 而类是我们的下层 也就是可以拿类给接口赋值 反之则不成立
执行结果:
1 | 【日志】执行addUser方法前,请求参数校验通过 |
但是代理类存在很多缺点:
代码冗余严重:一个目标类对应一个代理类,如果项目中有 100 个业务类,就要写 100 个代理类,开发效率极低,维护成本爆炸。
扩展性极差:如果业务接口新增一个方法,所有的代理类都要同步修改,违反了开闭原则(对扩展开放,对修改关闭)。
耦合度高:代理类和目标类强绑定,代理类只能代理指定的目标类,增强逻辑无法灵活复用。
而动态代理就能很好地解决这些问题
动态代理
学习了静态代理之后就来学习动态代理 动态代理主要依赖两个东西 一个代理类InvocationHandler一个中间类Proxy
其中Invocation就是我们实现增强的地方 而Proxy呢更多的可以把它理解成一个傀儡 当它被调用时 它会去找到InvocationHandler 然后InvocationHandler就来执行对应操作