Java 每日一刊(第21期):反射机制

news2024/11/28 8:31:59

在这里插入图片描述

文章目录

    • 前言
    • 动态插件系统
      • 面临的问题
      • 如何在运行时动态加载和调用类与方法
      • 设计模式的尝试
      • 引入反射
    • Java 反射的核心概念
      • Class 类
      • Constructor 类
      • Method 类
      • Field 类
    • Java 反射的应用场景
      • 框架开发
      • 插件系统
      • 序列化与反序列化
      • 动态代理
      • 测试工具
    • 反射的优缺点
    • 反射实战
      • 动态加载类并调用方法
      • 访问和修改私有字段
    • 本期小知识

前言

这里是分享 Java 相关内容的专刊,每日一更。

本期将为大家带来以下内容:

  1. 动态插件系统
  2. Java 反射的核心概念
  3. Java 反射的应用场景
  4. 反射的优缺点
  5. 反射实战
  6. 本期小知识

动态插件系统

假设你正在开发一个应用程序,需要设计一个 动态插件系统。这个系统的核心需求包括:

  1. 动态加载插件:用户可以在应用运行时添加新的插件,而这些插件的类和方法在编写核心代码时是未知的。
  2. 灵活调用方法:系统需要根据用户的选择或配置,动态调用插件中的特定方法。
  3. 无需修改核心代码:每次添加新插件时,核心代码无需进行修改或重新编译。

面临的问题

在传统的静态绑定机制下,程序必须在编译时明确知道所有可能调用的类和方法。这对于动态插件系统来说,显然无法满足需求。具体问题包括:

  1. 无法提前知道插件类名:插件类可能由用户或第三方开发,核心程序在编写时无法预知。
  2. 方法调用的动态性:插件中的方法名称和签名在编译时未知,需要在运行时动态确定并调用。
  3. 扩展性和维护性:每次新增插件,必须手动修改核心代码,增加维护成本和出错风险。

如何在运行时动态加载和调用类与方法

面对静态绑定的局限性,开发者提出了一个核心问题:

如何在不修改核心代码的情况下,根据运行时的需求,动态加载未知的类并调用其中的方法?

这个问题在以下场景中尤为突出:

  • 插件系统:用户或第三方开发者可以添加新的功能模块(插件),系统需要在运行时加载并执行这些插件。
  • 框架开发:框架需要根据配置或注解,在运行时动态管理对象的创建和注入。
  • 动态代理:在运行时生成代理对象并拦截方法调用。

设计模式的尝试

在反射技术引入之前,开发者尝试使用一些设计模式来解决动态加载和调用类与方法的问题。其中,工厂模式策略模式 是常见的选择。

通过工厂模式,开发者可以根据传入的标识符返回不同的类实例。例如:

public interface Plugin {
    void execute();
}

public class PluginA implements Plugin {
    @Override
    public void execute() {
        System.out.println("执行 PluginA");
    }
}

public class PluginB implements Plugin {
    @Override
    public void execute() {
        System.out.println("执行 PluginB");
    }
}

public class PluginFactory {
    public static Plugin getPlugin(String pluginName) {
        if ("PluginA".equals(pluginName)) {
            return new PluginA();
        } else if ("PluginB".equals(pluginName)) {
            return new PluginB();
        }
        return null;
    }
}

使用工厂类来创建插件实例:

public class PluginManager {
    public static void main(String[] args) {
        Plugin plugin = PluginFactory.getPlugin("PluginA");
        if (plugin != null) {
            plugin.execute();
        }
    }
}

尽管工厂模式在一定程度上提高了代码的灵活性,但它存在显著的局限性:

  • 扩展性差:每当有新插件时,必须手动修改 PluginFactory,添加新的判断逻辑。
  • 方法调用仍然是静态的:工厂模式只能动态返回对象,但方法的调用仍需在编译时确定。

这种设计模式虽然提供了一定的灵活性,但无法完全满足在运行时动态加载和调用类与方法的需求。

引入反射

为了解决动态加载类和方法的问题,开发者们开始借鉴 动态语言(如 Python、Ruby)的思想,引入了 **元编程 **的概念。元编程允许程序在运行时检查和操作自身的结构,反射 就是元编程的一种实现形式。

元编程与反射:

  • 元编程(Metaprogramming):指的是 编写能够操作、生成、修改代码的程序。元编程可以在编译时或运行时进行。
  • 反射(Reflection):是一种运行时元编程技术,允许程序在运行时获取类的信息,并动态操作类的构成部分(如方法、字段等)。

反射的引入使得 Java 具备了类似动态语言的能力,能够在运行时动态加载类、创建对象并调用方法,从而解决了静态绑定带来的灵活性不足问题。

Java 反射的核心概念

反射机制依赖于 java.lang.reflect 包中的几个核心类和接口,理解这些概念是掌握反射的基础。

Class 类

Class 类是反射的核心,用于表示类的元数据。开发者可以通过 Class 对象获取类的详细信息,包括类名、方法、字段等。

获取 Class 对象的三种方式:

  1. 通过类名字符串:

    Class<?> clazz = Class.forName("com.example.MyClass");
    
  2. 通过对象实例:

    MyClass obj = new MyClass();
    Class<?> clazz = obj.getClass();
    
  3. 通过类字面量:

    Class<?> clazz = MyClass.class;
    

Constructor 类

Constructor 类代表类的构造函数,允许开发者在运行时创建类的实例。

Constructor<?> constructor = clazz.getConstructor(String.class);
Object instance = constructor.newInstance("张三");

Method 类

Method 类代表类中的方法,可以通过反射获取方法并在运行时调用。

Method method = clazz.getMethod("execute");
method.invoke(instance);

Field 类

Field 类代表类的字段,允许开发者在运行时获取或修改类的字段值。

Field field = clazz.getDeclaredField("name");
field.setAccessible(true); // 访问私有字段
field.set(instance, "张三");

Java 反射的应用场景

反射在实际开发中的应用非常广泛,尤其在以下几个场景中尤为突出:

框架开发

反射被广泛应用于各种框架的开发中,如 Spring、Hibernate 等。这些框架通过反射实现了动态的依赖注入、对象关系映射等功能,极大地简化了开发过程。

Spring 框架通过反射机制动态创建和注入 Bean,管理对象之间的依赖关系。开发者无需显式地创建对象实例,Spring 会根据配置或注解自动完成对象的创建和注入。

插件系统

插件系统需要在运行时根据用户选择加载插件。反射提供了一种灵活的方式来加载用户定义的插件类,并动态调用其中的方法。

public interface Plugin {
    void execute();
}

public class PluginA implements Plugin {
    @Override
    public void execute() {
        System.out.println("执行 PluginA");
    }
}

public class PluginManager {
    public static void main(String[] args) {
        try {
            String pluginClassName = "PluginA"; // 通过配置文件或用户输入获取
            Class<?> pluginClass = Class.forName(pluginClassName);
            Plugin plugin = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
            Method executeMethod = pluginClass.getMethod("execute");
            executeMethod.invoke(plugin);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

序列化与反序列化

在处理 JSON、XML 等数据格式的序列化与反序列化过程中,反射被广泛使用,用于动态构造对象并设置其字段值。例如,使用反射进行反序列化

public class JsonDeserializer {
    public static <T> T deserialize(String json, Class<T> clazz) {
        try {
            T instance = clazz.getDeclaredConstructor().newInstance();
            JSONObject jsonObject = new JSONObject(json);
            for (String key : jsonObject.keySet()) {
                Field field = clazz.getDeclaredField(key);
                field.setAccessible(true);
                field.set(instance, jsonObject.get(key));
            }
            return instance;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

动态代理

Java 的动态代理机制依赖于反射,通过生成代理类来拦截方法调用,实现 AOP(面向切面编程)等功能。例如:

// 定义服务接口
public interface Service {
    void perform();
}

// 服务实现类
public class ServiceImpl implements Service {
    @Override
    public void perform() {
        System.out.println("执行服务逻辑");
    }
}

// 代理处理器
public class ServiceProxy implements InvocationHandler {
    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("方法调用前");
        Object result = method.invoke(target, args);
        System.out.println("方法调用后");
        return result;
    }
}

// 动态代理演示
public class ProxyDemo {
    public static void main(String[] args) {
        Service service = new ServiceImpl();
        Service proxyInstance = (Service) Proxy.newProxyInstance(
            service.getClass().getClassLoader(),
            service.getClass().getInterfaces(),
            new ServiceProxy(service)
        );
        proxyInstance.perform();
    }
}

运行结果:

方法调用前
执行服务逻辑
方法调用后

测试工具

在测试框架如 JUnit 中,反射被用于动态调用测试方法,无需在编译时明确指定每个测试用例的方法名。这使得测试框架能够自动发现并执行测试方法。

反射的优缺点

优点缺点
1. 灵活性和动态性1. 性能开销
反射允许程序在运行时动态加载类、创建对象并调用方法,极大地提高了程序的灵活性和扩展性。反射的运行时操作比静态调用慢得多,因为反射涉及运行时的类型检查和方法查找,尤其在频繁使用反射的场景中,性能瓶颈更为明显。
2. 通用性2. 安全性问题
通过反射,开发者可以编写通用的代码,处理不同的类和对象,而无需在编译时明确指定具体类型。反射允许访问和修改类的私有成员,可能破坏封装性,导致安全漏洞。在高安全性应用中需要谨慎使用反射。
3. 框架支持3. 可维护性差
反射是许多Java框架(如Spring、Hibernate)的核心技术,支持依赖注入、对象关系映射等高级功能。反射代码较为复杂,绕过了编译时的类型检查,容易导致运行时错误,增加了调试和维护的难度。
4. 动态代理4. 违反封装原则
反射使得动态代理成为可能,支持AOP(面向切面编程)等编程范式。反射可以访问和修改类的私有字段和方法,破坏了类的封装性,使得内部实现暴露给外部,增加了代码耦合度。
5. 测试工具5. 编译时检查缺失
反射支持测试框架自动发现和执行测试方法,提升了测试效率。通过反射调用方法时,编译器无法进行类型检查,任何方法名或参数错误都会在运行时抛出异常。

反射实战

为了更好地理解反射的使用,以下将通过具体的代码示例展示如何动态加载类、调用方法、访问和修改字段。

动态加载类并调用方法

以下示例展示了如何使用反射机制动态加载类、创建对象实例并调用其方法。

  1. 定义插件接口和实现类:

    // 定义插件接口
    public interface Plugin {
        void execute();
    }
    
    // PluginA 实现类
    public class PluginA implements Plugin {
        @Override
        public void execute() {
            System.out.println("执行 PluginA");
        }
    }
    
    // PluginB 实现类
    public class PluginB implements Plugin {
        @Override
        public void execute() {
            System.out.println("执行 PluginB");
        }
    }
    
  2. 使用反射加载并调用插件方法:

    public class PluginManager {
        public static void main(String[] args) {
            try {
                // 假设插件类名通过配置文件或用户输入获取
                String pluginClassName = "PluginA"; // 可以替换为 "PluginB"
    
                // 动态加载类
                Class<?> pluginClass = Class.forName(pluginClassName);
    
                // 动态创建类的实例
                Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
    
                // 动态获取 execute 方法
                Method executeMethod = pluginClass.getMethod("execute");
    
                // 动态调用 execute 方法
                executeMethod.invoke(pluginInstance);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

运行结果:

执行 PluginA

如果将 pluginClassName 改为 "PluginB",则输出:

执行 PluginB

访问和修改私有字段

以下示例展示了如何使用反射访问和修改类的私有字段。

public class ReflectionFieldExample {
    private String secret = "初始秘密";

    public static void main(String[] args) {
        try {
            ReflectionFieldExample example = new ReflectionFieldExample();

            // 获取 Class 对象
            Class<?> clazz = example.getClass();

            // 获取 private 字段 'secret'
            Field secretField = clazz.getDeclaredField("secret");

            // 设置访问权限
            secretField.setAccessible(true);

            // 获取字段值
            String secretValue = (String) secretField.get(example);
            System.out.println("原始秘密: " + secretValue);

            // 修改字段值
            secretField.set(example, "修改后的秘密");
            String newSecretValue = (String) secretField.get(example);
            System.out.println("修改后的秘密: " + newSecretValue);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果:

原始秘密: 初始秘密
修改后的秘密: 修改后的秘密

本期小知识

static final修饰的字段在编译时会被内联,这意味着 Java 编译器将其值直接嵌入到字节码中,导致通过反射修改这些字段的值无效。例如:

class MyClass {
    public static final int MY_CONSTANT = 10;
}

public class ReflectionTest {
    public static void main(String[] args) throws Exception {
        Field field = MyClass.class.getDeclaredField("MY_CONSTANT");
        field.setAccessible(true);
        field.set(null, 100);  // 试图修改MY_CONSTANT
        System.out.println(MyClass.MY_CONSTANT);  // 仍输出: 10
    }
}

这说明即使通过反射修改了字段,结果也不会生效,因为编译器已经将常量值直接嵌入到使用它的代码中。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2189565.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【hot100-java】【将有序数组转换为二叉搜索树】

二叉树篇 BST树 递归直接实现。 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNo…

【C++差分数组】2381. 字母移位 II|1793

本文涉及知识点 C差分数组 LeetCode2381. 字母移位 II 给你一个小写英文字母组成的字符串 s 和一个二维整数数组 shifts &#xff0c;其中 shifts[i] [starti, endi, directioni] 。对于每个 i &#xff0c;将 s 中从下标 starti 到下标 endi &#xff08;两者都包含&#…

STM32的串行外设接口SPI

一、SPI简介 1.SPI总线特点 &#xff08;1&#xff09;四条通信线 SPI需要SCK、MISO、MOSI、NSS四条通信线来完成数据传输 &#xff0c;每增加一个从机&#xff0c;多一条NSS通信线。 &#xff08;2&#xff09;多主多从 SPI总线允许有多个主机和多个从机。 &#xff08;3&…

再见 ESNI,你好 ECH!—— ECH的前世今生

译者注&#xff1a;2024 年 9 月 25 日&#xff0c;Cloudflare 宣布再次推出 ECH 功能。借此契机&#xff0c;本人翻译了 Cloudflare 介绍 ECH 的博文 Good-bye ESNI, hello ECH! &#xff0c;以便科普ECH的发展历程。 现代互联网上的大多数通信都经过加密&#xff0c;以确保其…

Flink源码剖析

写在前面 最近一段时间都没有更新博客了&#xff0c;原因有点离谱&#xff0c;在实现flink的两阶段提交的时候&#xff0c;每次执行自定义的notifyCheckpointComplete时候&#xff0c;好像就会停止消费数据&#xff0c;完成notifyComplete后再消费数据&#xff1b;基于上述原因…

在Stable Diffusion WebUI中安装SadTalker插件时几种错误提示的处理方法

SD中的插件一般安装比较简单&#xff0c;但也有一些插件安装会比较难。比如我在安装SadTalker时&#xff0c;就遇到很多问题&#xff0c;一度放弃了&#xff0c;后来查了一些网上攻略&#xff0c;自己也反复查看日志&#xff0c;终于解决&#xff0c;不吐不快。 一、在Stable …

ElasticSearch高级功能详解与读写性能调优

目录 1. ES数据预处理 1.1 Ingest Node Ingest Node VS Logstash 1.2 Ingest Pipeline Pipeline & Processor 创建pipeline 使用pipeline更新数据 借助update_by_query更新已存在的文档 1.3 Painless Script Painless的用途&#xff1a; 通过Painless脚本访问字…

(17)MATLAB使用伽马(gamma)分布生成Nakagami-m分布的方法1

文章目录 前言一、使用伽马分布生成Nakagami分布随机变量的方法一二、MATLAB仿真代码后续 前言 MATLAB在R2013a版本中引入Nakagami分布对象&#xff0c;可以用来生成Nakagami随机变量。但是在更早的MATLAB版本中&#xff0c;并没有可以直接生成 Nakagami分布的随机变量的内置的…

C++之多态篇(超详细版)

1.多态概念 多态就是多种形态&#xff0c;表示去完成某个行为时&#xff0c;当不同的人去完成时会有不同的形态&#xff0c;举个例子在车站买票&#xff0c;可以分为学生票&#xff0c;普通票&#xff0c;军人票&#xff0c;每种票的价格是不一样的&#xff0c;当你是不同的身…

【JAVA开源】基于Vue和SpringBoot的旅游管理系统

本文项目编号 T 063 &#xff0c;文末自助获取源码 \color{red}{T063&#xff0c;文末自助获取源码} T063&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析5.4 用例设计 六、核…

【STM32开发之寄存器版】(二)-USART

一、前言 串口作为STM32的重要外设&#xff0c;对程序调试具有不可替代的作用。通用同步异步收发器(USART)提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换。USART利用分数波特率发生器提供宽范围的波特率选择。其主要具备以下特性&am…

Nacos入门指南:服务发现与配置管理的全面解析

Nacos 是一个用于动态服务发现、配置管理和服务管理的平台。它由阿里巴巴开源&#xff0c;旨在帮助开发者更轻松地构建云原生应用。Nacos 支持多种环境下的服务管理和配置管理&#xff0c;包括但不限于 Kubernetes、Docker、虚拟机等。 一、Nacos的主要功能 1. **服务发现与健康…

GS-SLAM论文阅读笔记-CaRtGS

前言 这篇文章看起来有点像Photo-slam的续作&#xff0c;行文格式和图片类型很接近&#xff0c;而且貌似是出自同一所学校的&#xff0c;所以推测可能是Photo-slam的优化与改进方法&#xff0c;接下来具体看看改进了哪些地方。 文章目录 前言1.背景介绍GS-SLAM方法总结 2.关键…

认知杂谈97《兼听则明,偏听则暗》

内容摘要&#xff1a; 在信息爆炸的时代&#xff0c;我们被各种信息包围&#xff0c;这些信息往往经过精心设计以吸引注意力和影响观点。为了避免被操控&#xff0c;我们需要从多个渠道获取信息&#xff0c;并培养批判性思维来分析信息的真实性和偏见。 提高信息素养&#xff0…

读数据湖仓07描述性数据

1. 描述性数据 1.1. 基础数据中包含不同类型的数据&#xff0c;而不同类型数据的描述性数据也存在显著的差异 1.2. 尽管这些描述性数据存在根本性的差异&#xff0c;但通过描述性数据&#xff0c;我们可以全面了解基础数据中的数据 1.3. 通过分析基础设施中提供的描述性数据…

基于CAN总线的STM32G4 Bootloader设计说明

1 设计目的 根据芜湖铂科新能源自身企业发展需要&#xff0c;开发一款基于ST公司STM32G4系列MCU&#xff08;具体开发用型号STM32G473和STM32G431微处理器&#xff09;的CAN总线bootloader&#xff0c;方便应用程序的刷写。CAN设备采用周立功CAN卡&#xff08;USBCAN-II、CAN-…

Docker安装人大金仓(kingbase)关系型数据库教程

人大金仓数据库(KingbaseES)是由中国人民大学金仓公司研发的一款自主知识产权的关系型数据库管理系统。 官网地址:https://www.kingbase.com.cn/ 本章教程,主要介绍如何用Docker安装启动人大金仓(kingbase)关系型数据库。 一、下载镜像 下载地址:https://www.kingbase.c…

【黑马软件测试三】web功能测试、抓包

阶段三&#xff0c;内容看情况略过 Web功能测试链接测试表单测试搜索测试删除测试cookies/session测试数据库测试抓包工具的使用一个APP的完整测试流程熟悉APP业务流程功能测试APP专项测试兼容性安装、卸载和升级交叉测试(干扰测试)push消息测试用户体验测试 Web功能测试 通过…

Python画笔案例-075 绘制趣味正方形

1、绘制趣味正方形 通过 python 的turtle 库绘制 趣味正方形,如下图: 2、实现代码 绘制趣味正方形,以下为实现代码: """趣味正方形.py画个正方形后,单击它会移动,并且碰到边缘就反弹。这个版本采用画布的move命令让当前线条项目移动实现的。也可以用纯动画…

华夏ERP账号密码泄露漏洞

漏洞描述 华夏ERP账号密码泄露漏洞 漏洞复现 FOFA "jshERP-boot" POC IP/jshERP-boot/user/getAllList;.ico