动态代理与反射

1.动态代理

为什么需要代理?

假设你是个明星(BigStar),工作是唱歌、跳舞。但每次演出前,经纪人(代理)会帮你:

  1. ​谈合同​(权限检查)
  2. ​安排档期​(日志记录)
  3. ​收钱​(事务管理)

你不用自己处理这些杂事,经纪人(动态代理)帮你搞定!这就是动态代理的作用

动态代理是能帮你​​增强​​某个对象的功能,而不用直接修改它的代码。

作用

  1. 避免重复代码

    • 比如你想给多个方法都加上日志记录,如果手动在每个方法里写 System.out.println(...),代码会变得臃肿。动态代理可以统一处理这些逻辑,让代码更简洁。
  2. 灵活增强功能

    • 你可以在​方法执行前​ (比如检查权限)、​执行后​ (比如记录结果)、甚至​替换执行逻辑​(比如缓存数据)。
  3. 适用于框架开发

    • Spring、MyBatis 等框架都用动态代理来实现 AOP(面向切面编程),比如自动管理事务、日志、性能监控等。
  4. 解耦业务逻辑和通用逻辑

    • 比如业务代码只关心核心逻辑,而日志、权限等交给代理处理,让代码更清晰。

实现动态代理

以下是简单的 Java 动态代理示例

如果需要代理有接口的类,可以基于 JDK 原生的 ProxyInvocationHandler 实现

java 复制代码
public class UserServiceImpl implements UserService {
    @Override
    public void login(String username) {
        System.out.println(username + " 登录成功!");
    }
}
java 复制代码
package com.vv.util;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

// 代理逻辑处理器:在方法调用前后插入额外逻辑
public class MyProxyHandler implements InvocationHandler {
    private final Object target; // 被代理的真实对象

    public MyProxyHandler(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("【代理】准备调用方法: " + method.getName());
        Object result = method.invoke(target, args); // 调用真实对象的方法
        System.out.println("【代理】方法调用完成");
        return result;
    }
}
java 复制代码
public class Main {
    public static void main(String[] args) {
        UserService userService = new UserServiceImpl();
        MyProxyHandler handler = new MyProxyHandler(userService);
        // 动态生成代理对象
        //参数一:用于指定用哪个类加载器,去加载生成的代理类
        //参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
        //参数三:用来指定生成的代理对象要干什么事情
        UserService proxy = (UserService) Proxy.newProxyInstance(
                userService.getClass().getClassLoader(),
                userService.getClass().getInterfaces(),
                handler);
        proxy.login("Jack");
    }
}

如果需要代理没有接口的类,可以使用 ​​CGLIB​​(如 Spring AOP 默认方式)

2.反射

想象你有一个玩具箱(JVM),里面装了很多玩具(Java 类)。正常情况下,你只能通过玩具的说明书(编译时的代码)来玩它们。但反射就像给你一个"魔法镜",让你能直接看到玩具箱里有什么玩具(类)、玩具的零件(字段)、玩具的功能(方法),甚至能直接操作它们------不需要提前知道玩具的具体细节

快速入门

读取配置文件的类名和方法名,来创建类和调用方法

properties 复制代码
class_path=com.vv.bean.Cat
method_name=say
java 复制代码
//读取配置文件要创建的类和要调用的方法
Properties properties = new Properties();
properties.load(Files.newInputStream(Paths.get("src\main\resources\reflect.properties")));
String classPath = properties.get("class_path").toString();
String methodName = properties.get("method_name").toString();

//传统方法 行不通
//new classPath()

//使用反射
Class<?> cls = Class.forName(classPath); 
Object obj = cls.newInstance();
Method method = cls.getMethod(methodName);
method.invoke(obj);

注意:Class对象,包含了Cat类的完整信息,就像一面镜子透过它看到类的结构,所以称为反射

这样的需求在框架用的特别多,即通过外部文件配置,在不修改源码情况下来控制程序,也符合设计模式的 ocp 原则(开闭原则: 不修改源码,扩容功能)

反射的缺点就是:执行效率低

反射机制

Java反射的核心是基于JVM的类加载机制Class对象

当Java程序运行时,JVM会将每一个加载的类(.class文件)转换成一个java.lang.Class对象,这个对象包含了该类的所有结构信息(如方法、字段、构造方法等)。反射就是通过这个Class对象来动态访问和操作类的成员。并且普通的对象可以得到它对应的Class对象!

Class类

  1. Class和普通类一样,因此也继承Object类
  2. Class类对象不是new出来的,而是通过ClassLoader加载出来的
  3. 对于某个类的Class类对象,在内存中只有一份,因为类只加载一次
  4. 每个类的实例都会记得自己是由哪个 Class 实例所生成
  5. 通过Class可以完整地得到一个类的完整结构,通过一系列APIClass对象是存放在堆的

反射机制可以完成:

  1. 在运行时判断任意一个对象所属的类
  2. 在运行时构造任意一个类的对象
  3. 在运行时得到任意一个类所具有的成员变量和方法
  4. 在运行时调用任意一个对象的成员变量和方法
  5. 生成动态代理

反射相关类

通过Class类对象:

获得类相关的方法

获得类中属性相关的方法

获得类中构造器相关的方法

获得类中方法相关的方法

Class类的获取方式

方式一:

  • 前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException
  • 实例:Class cls1 =Class.forName( "java.lang.Cat" )
  • 应用场景:多用于配置文件,读取类全路径,加载类

方式二:

  • 前提:若已知具体的类(Class已经创建),通过类的class 获取,该方式最为安全可靠,程序性能最高
  • 实例:Class cls2 = Cat.class
  • 应用场景:多用于参数传递,比如通过反射得到对应构造器对象

方式三:

  • 前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象
  • 实例:Class clazz= 对象.getClass();//运行类型
  • 应用场景: 通过创建好的对象,获取Class对象

其他方式

  • ClassLoader cl=对象.getClass().getClassLoader();
  • Class clazz4 = cl.loadClass("类的全类名");

注意:如下类型有Class对象

外部类、成员内部类,静态内部类,局部内部类,匿名内部类、接口、数组、枚举、注解、基本数据类型、void

类加载

1.加载阶段

JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象

2.1连接-验证阶段

目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

包括:文件格式验证(是否以魔数 oxcafebabe开头,魔数一般用于区分文件的种类与用来检测这个文件有没有受到损坏)、元数据验证、字节码验证和符号引用验证

可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。

2.2连接-准备阶段

JVM 会在该阶段对静态变量,分配内存并初始化(对应数据类型的默认初始值,如 0、null、false 等)。

这些变量所使用的内存都将在方法区中进行分配

比如类有如下三个成员变量

2.3连接-解析阶段

虚拟机将常量池中的符号引用(编译时生成的抽象引用)转换为直接引用(运行时可用的具体引用)。

  • 符号引用:与虚拟机内存布局无关的描述性引用(如全限定名、描述符等)
  • 直接引用:与虚拟机内存布局相关的具体指针、偏移量或句柄

3.初始化阶段

初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行()方法的过程,该方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并

虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕

反射创建实例

方式一: 调用类中的public修饰的无参构造器

方式二: 调用类中的指定构造器

Student.class

java 复制代码
public class Student {

    private String name;
    private int age;

    //无参构造方法
    public Student(){
        this.name = "default_name";
        this.age = 0;
    }

    //有参数构造方法
    public Student(String name ,int age){
        this.name = name;
        this.age = age;
    }

    //私有构造方法
    private Student(int age){
        this.name = "default_name";
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

main

java 复制代码
public static void main(String[] args) throws Exception {
    //加载Class对象
    Class clazz = Class.forName("com.hmdp.other.Student");

    //通过public的无参构造创建实例
    Object o1 = clazz.newInstance();
    System.out.println(o1); // Student{name='default_name', age=0}

    //通过public的有参构造创建实例
    Constructor constructor = clazz.getConstructor(String.class, int.class);
    Object o2 = constructor.newInstance("zhangshan", 18);
    System.out.println(o2); // Student{name='zhangshan', age=18}

    //通过私有的有参构造创建实例
    Constructor declaredConstructor = clazz.getDeclaredConstructor(int.class);
    declaredConstructor.setAccessible(true); // 爆破 (可以访问私有的构造器、属性、方法)
    Object o3 = declaredConstructor.newInstance(20);
    System.out.println(o3); // Student{name='default_name', age=20}
}

同理可以操作属性、方法等... ...

相关推荐
浩宇软件开发几秒前
Android开发,实现一个简约又好看的登录页
android·java·android studio·android开发
brzhang几秒前
告别『上线裸奔』!一文带你配齐生产级 Web 应用的 10 大核心组件
前端·后端·架构
shepherd1111 分钟前
Kafka生产环境实战经验深度总结,让你少走弯路
后端·面试·kafka
南客先生7 分钟前
多级缓存架构设计与实践经验
java·面试·多级缓存·缓存架构
anqi2710 分钟前
如何在 IntelliJ IDEA 中编写 Speak 程序
java·大数据·开发语言·spark·intellij-idea
袋鱼不重14 分钟前
Cursor 最简易上手体验:谷歌浏览器插件开发3s搞定!
前端·后端·cursor
m0_7401546716 分钟前
maven相关概念深入介绍
java·maven
嘻嘻哈哈开森16 分钟前
Agent 系统技术分享
后端
用户40993225021217 分钟前
异步IO与Tortoise-ORM的数据库
后端·ai编程·trae
会有猫22 分钟前
LabelStudio使用阿里云OSS教程
后端