Java 基础
Java 介绍
一个Java 程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来协同工作,下面简要介绍下类、对象、方法和实例变量的概念。
- 对象:对象是类的一个实例,有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃 等。
- 类:是一个模板,它描述了一类对象的行为和状态。
- 方法:方法就是行为,一个类可以有很多个方法。逻辑运算、数据修改、以及所有的动作都是在方法中完成的。
- 实例变量:每个对象都有独特的实例变量,对象的状态由这些实例变量的值决定。
实例变量也被称为类变量,是声明在内部类的上面,公共类的下面:
Java 类加载相关
Java 依赖 JVM 实现跨平台,一个 Java 程序在运行前会被编译为字节码文件( .class ),在类初始化时调
用 java.lang.ClassLoader 加载类字节码。
这是编译后的class文件的内容:
在Java中,类加载器(ClassLoader)是负责将Java类加载到JVM中以供程序运行的重要组件。类加载器采用双亲委派模型,当一个类需要被加载时,首先由当前类加载器尝试加载,如果当前类加载器无法完成加载,则委托给其父类加载器,依此类推,直至Bootstrap ClassLoader。这种层级结构的设计可以确保类的唯一性,避免重复加载。
在Java中,我们可以使用getClassLoader()
方法来获取某个类的加载器:
ClassLoader loader = ClassName.class.getClassLoader();
然而,需要注意的是,如果一个类是由Bootstrap ClassLoader加载的,调用getClassLoader()
方法将返回null。这是因为Bootstrap ClassLoader是由C++编写,不是普通的Java类,因此没有对应的Java对象来表示。Bootstrap ClassLoader是在JVM启动时由JVM自身加载的,它负责加载JRE核心库,包括rt.jar
等核心类库。因此,对于由Bootstrap ClassLoader加载的类,我们无法通过getClassLoader()
方法获取其加载器。
这个特性对于理解类加载机制和调试ClassLoader相关的问题非常重要。当需要确保某个类是由Bootstrap ClassLoader加载时,可以简单地通过判断是否返回null来进行验证。
ClassLoader 类核心方法:
- loadClass ,加载指定的 Java 类
- findClass ,查找指定的 Java 类
- findLoadedClass ,查找 JVM 已经加载过的类
- defineClass ,定义一个 Java 类
- resolveClass ,链接指定的 Java 类
Java 类加载方式
加载类的过程通常涉及到Java反射机制。在Java中,反射机制允许程序在运行时检查和操作类、对象、属性以及方法。通过反射,我们可以动态地加载类、实例化对象、调用方法、访问/修改属性等。
ClassLoader负责将类的字节码加载到内存中,而反射机制允许我们在运行时检查和操作这些类。通过Class类及其相关API,我们可以获取类的构造函数、方法、字段等信息,并在运行时实例化对象、调用方法等。因此,在动态加载类以及对类进行各种操作的过程中,我们通常会使用反射机制。
在实际开发中,通过反射机制,我们可以实现一些灵活的设计,比如实现插件化系统、动态配置对象、实现ORM框架等。当需要在运行时动态地处理类或对象,反射机制会成为一个非常有用的工具。
因此,类加载和反射机制通常是紧密相关的,它们共同构成了Java动态加载和运行时操作类的基础。
Java 的动态加载机制
Java动态加载是指在程序运行时才加载类和资源,而不是在编译时就将所有类加载好。动态加载能够使程序更加灵活,可以根据具体情况在需要时加载所需的类,而不是提前将所有类加载到内存中。
动态加载的过程一般如下:
- 识别需要加载的类/资源:程序在运行时根据具体条件或需要动态地识别需要加载的类或资源。
- 定位类/资源:一旦确定需要加载哪些类或资源,程序会根据类的全限定名或者资源的路径来定位具体的类文件(.class文件)或资源文件。
- 通过类加载器加载类/资源:通过调用类加载器的相应方法,将目标类的字节码文件加载到内存。如果是资源文件,也可以通过类加载器来获取相应的输入流。
- 使用反射进行操作:一旦类或资源加载到内存中,程序可以通过反射机制获取类的构造函数、实例化对象、调用方法等。
Java中常见的动态加载方式包括使用Class.forName()
方法来动态加载类,以及使用类加载器(ClassLoader)的各种实现来动态加载类和资源文件。
总的来说,动态加载能够使Java程序更加灵活,它允许程序在运行时根据具体情况加载所需的类和资源,而不需要将所有类提前加载好。
Java 的反射机制
Java的反射机制允许程序在运行时动态地检查、获取并操作类的成员属性和成员方法,甚至可以在运行时创建对象,并通过对象调用类中的成员方法。通过反射,程序可以遍历类的构造函数、方法、字段等,获取它们的修饰符、注解、参数类型等信息。这使得开发者能够在运行时动态地操作类的结构,并且可以实现灵活的设计和编程。
通过反射机制,我们可以动态地实例化类并调用类中的方法。
在大多数情况下,反射时动态实例化的类通常是已经存在并且被编译好的。也就是说,这些类已经存在于程序的类路径中,程序能够通过类加载器获得这些类的 Class 对象,然后使用反射机制来实例化和操作这些类。
import javax.tools.JavaCompiler; import javax.tools.ToolProvider; import java.io.File; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; public class DynamicClassLoadingExample { public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException { String className = "DynamicClass"; String sourceCode = "public class DynamicClass { public void dynamicMethod() { System.out.println(\"Dynamically loaded method\"); } }"; // 写源代码到文件 File sourceFile = new File(className + ".java"); Files.write(sourceFile.toPath(), sourceCode.getBytes()); // 编译源代码 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); compiler.run(null, null, null, sourceFile.getPath()); // 加载类 URLClassLoader classLoader = URLClassLoader.newInstance(new URL[] { sourceFile.getParentFile().toURI().toURL() }); Class<?> dynamicClass = Class.forName(className, true, classLoader); // 实例化类对象 Object obj = dynamicClass.newInstance(); // 调用方法 dynamicClass.getDeclaredMethod("dynamicMethod").invoke(obj); } }
这里的Java 代码演示了动态加载的过程,大致过程就是将源代码写到一个.java后缀的文件中,如果没有就创建这个文件,然后将该文件进行编译,编译成.class文件,当有需要的时候会从这个文件中动态的加载类并 进行实例化
但是java的反序列化漏洞也出现在这里,如果.class文件中有危险的类文件,那么就可以通过反射机制来调用这个类文件,从而实现攻击者的目的。
反射方法:
Class<?> dynamicClass = Class.forName(className, true, classLoader);
首先看一下 Class.forName 方法。这个方法是用于在运行时动态的加载类。它接受三个参数:类名、是否初始化、类加载器。
- 类名:这是一个字符串,表示要加载的类的全限定名称
- 是否初始化标记:这是一个boolean值,如果为true,则链接(加载和初始化)类,如果为false,则仅加载类。
- 类加载器:这是一个类加载器对象,用于加载类。
所以这行代码的作用是使用给定的类加载器加载指定名称的类。在这里,classname是需要加载的目标类名称,true表示需要初始化,classLoader是使用URLClassLoader 创建的类加载器对象。
ClassLoader 加载
this.getClass().getClassLoader().loadClass("com.adan0s.test.TestHello");
这段代码使用的当前的类加载器来加载名为"com.adan0s.test.TestHello" 的类。
首先getClass 方法用于获取当前对象的类。然后,getClassLoader方法返回当前类的类加载器,接着是loadClass()方法,它是类加载器的一个方法,用于 加载指定名称的类。在这里"com.adan0s.test.TestHello" 是需要被加载的类名。
这两段代码都是用于运行时加载类,但是实现的方式有所区别。
- 反射方法
- 通过反射方法加载的类使用了class.forName() 方法,它是 java.lang.Class 的静态方法,用于在运行时通过类的完全限定名称来动态的加载类。该方法会初始化被加载的类。
- ClassLoader 加载
- 使用了类加载器loadClass() 方法,它是类加载器的实例方法,用于通过类的完全限定名称来加载类,这里使用了当前类的类加载器来加载指定的类。
从功能上来说,两者都可以用来加载类,但是在实际应用中,Class.forName() 更常用,因为它可以方便的通过类的完全限定名称加载类并立即初始化它。而使用类加载器的 loadClass() 方法,通常用于更复杂的类加载场景,例如自定义类加载逻辑或者动态加载类时更为灵活。
ClassLoader 类加载器
在 Java 中,ClassLoader 是一个抽象类,它是类加载器的基类。根据Java 类加载器体系结构,ClassLoader 又分为以下几种类型:
- 启动类加载器(Bootstrap ClassLoader)
- 负责加载 Java 核心类,这部分类库存放在JAVA_HOME/jre/lib 目录下,由C++实现,是虚拟机自身的一部分。由于引导类加载器涉及到虚拟机本地实现细节,因此在 Java 中并没有直接对应的 Java 类。
- 扩展类加载器(Extension ClassLoader)
- 负责加载 java 平台扩展的 jar 文件,即 JAVA_HOME/jre/lib/ext 目录中的jar 包,在java中对应的类是 sun.misc.Launcher$ExtClassLoader
- 应用程序类加载器(Application ClassLoader)
- 负责加载应用程序路径上的类,通过环境变量 CLASSPATH 指定。在java中对应的类是sun.misc.Launcher$AppClassLoader
- 自定义类加载器(Custom ClassLoader)
- 开发人员可以通过继承以上三种类加载器的类,对其中的方法进行重写,编写自定义的类加载器,实现特定的类加载需求。用户自定义类加载器可以继承ClassLoader并覆盖它的findClass方法来实现自定义的类加载需求。
ClassLoader 类加载流程
ClassLoader是Java中用于加载类文件的核心组件之一。它负责在运行时将类的字节码文件加载到Java虚拟机中,并生成对应的Class对象。
public class CustomClassLoader extends ClassLoader { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } @Override protected final Class<?> findLoadedClass(String name) { // 检查类是否已被加载 if (checkName(name)) { return null; } return findLoadedClass(name); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 在此处查找并加载类的字节码 byte[] classBytes = loadClassBytes(name); if (classBytes == null) { throw new ClassNotFoundException(name); } else { return defineClass(name, classBytes, 0, classBytes.length); } } private byte[] loadClassBytes(String name) { // 加载类的字节码,这里可以根据实际情况进行处理 // 省略实际的字节码加载过程 return null; } private boolean checkName(String name) { // 检查加载类名的逻辑,这里可以根据实际情况进行处理 // 省略具体的逻辑 return false; } } public class Main { public static void main(String[] args) { try { CustomClassLoader customClassLoader = new CustomClassLoader(); Class<?> clazz = customClassLoader.loadClass("com.example.MyClass"); // 输出加载的类信息 System.out.println("Class loaded: " + clazz.getName()); } catch (ClassNotFoundException e) { System.out.println("Class not found: " + e.getMessage()); } } }
1、loadClass
ClassLoader 会调用 Class<?> loadClass(),传入要加载的类名:
public Class<?> loadClass(String name) throws ClassNotFoundException{ return loadClass(name,flase); }
这是一个public访问权限的方法,它接受一个字符串参数name,并且声明可能会抛出ClassNotFoundException 异常。方法的返回类型是Class<?>,表示加载到的类的类型。在这个方法中调用了另一个同名的loadClass方法,传入了name和false两个参数。
2、findLoadedClass
之后会调用findLoadedClass(),检查此类是否被初始化,如果已经初始化则返回类对象;
protected final Class<?> findLoadedClass(String name){ if(checkName(name)) return null; return finLoadedClass@(name); }
3、选择加载器
创建当前ClassLoader 时,如果传入了父类加载器,则使用父类加载器进行加载,否则使用 Bootstrap ClassLoader
if(parent != null){ c = parent.loadClass(name,false); }else{ c = findBootstrapClassOrNull(name); }
如果这里无法加载目标类,就会调用自身的findClass方法进行加载:
if(c == null){ long t1 = System.nanoTime(); c = findClass(name); sun.misc.PertCounter.getParentDelegationTime().addTime(t1 = t0); sun.misc.PertCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PertCounter.getFindClasses().increment(); }
如果并没有重新 findClass 方法,会直接返回类加载失败异常。如果重写并找到了对应类的字节码,会调用defineClass方法在 JVM 中注册该类:
4、resolveClass
如果在loadClass 时,传入了resolve 参数为true,那么还要调用resolveClass 方法链接类,默认为False
最终将返回一个被JVM 加载后的 java.lang.class 类对象。
自定义ClassLoader
这里使用的类加载器继承的ClassLoader 类加载器,ClassLoader 是所有类加载器的基类,而Java 已经预定义好的三种类也是继承于 ClassLoader 这个基类,从ClassLoader 这个基类扩展而来的。
package com.adanos.test; import java.lang.reflect.Method; // 自定义类加载器 public class MyclassLoader extends ClassLoader { // 定义类的全限定名 private static String className = "com.adanos.test.TestHello"; // 定义类的字节码 private static byte[] classBytes = new byte[]{...}; // 字节码数据 // 重写findClass方法 @Override public Class<?> findClass(String name) throws ClassNotFoundException { // 如果请求加载的类名与className相等 if (name.equals(className)) { // 定义类 return defineClass(className, classBytes, 0, classBytes.length); } // 否则,使用父类加载器加载 return super.findClass(name); } // 主方法 public static void main(String[] args) { try { // 实例化自定义类加载器 MyClassLoader myClassLoader = new MyClassLoader(); // 使用自定义加载器加载类 Class<?> testClass = myClassLoader.loadClass(className); // 通过反射创建类的实例 Object object = testClass.newInstance(); // 通过反射获取类的方法 Method method = object.getClass().getMethod("hello"); // 通过反射调用方法 String result = (String) method.invoke(object); // 输出结果 System.out.println(result); } catch (Exception e) { e.printStackTrace(); } } }
@Override 表示对父类中这个方法的重写,如果没有对父类中的这个方法重写的话,就不需要这样。
通过自定义 ClassLoader, 可以实现加载自定义的字节码。
主要步骤:
- 重写 findClass 方法,
- 调用 defineClass 方法时传入自定义的字节码内容
- 利用反射机制调用相应的方法
URLClassLoader
利用 URLClassLoader 可以加载远程资源,比如jar包,进行远程方法调用
首先准备好2个java文件的代码,第一个是对第二个进行远程调用,第二个是执行whoami这个命令的
import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; public class RemoteCmdExecution { public static void main(String[] args) { try { // 创建URL对象,指向远程的jar包文件 URL url = new URL("http://127.0.0.1:8080/Cmd.jar"); // 使用URL创建一个类加载器 URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url}); // 加载远程jar包中定义的类,确保这里的包名与Cmd类的实际包名一致 Class<?> cmdClass = urlClassLoader.loadClass("Cmd"); // 通过反射调用类的exec方法执行命令 Method execMethod = cmdClass.getMethod("exec", String.class); // 定义要执行的命令 String cmd = "whoami"; // 默认命令,如果需要可以根据情况修改 // 检查命令字符串是否为空 if (cmd == null || cmd.isEmpty()) { System.out.println("Command is empty. Using default command."); cmd = "whoami"; // 使用默认命令 } Process process = (Process) execMethod.invoke(null, cmd); // 获取命令执行结果的输入流 InputStream inputStream = process.getInputStream(); // 创建BufferedReader来读取命令执行结果 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String line; // 逐行读取命令执行结果并打印 while ((line = reader.readLine()) != null) { System.out.println(line); } // 关闭资源 reader.close(); // 等待命令执行完成并获取返回码 int exitCode = process.waitFor(); System.out.println("exit code:" + exitCode); } catch (Exception e) { // 打印出任何发生的异常 e.printStackTrace(); } } }
执行一下第二个代码
import java.io.*; // Cmd类 public class Cmd { // exec方法:执行命令并返回进程对象 public static Process exec(String cmd) throws IOException{ return Runtime.getRuntime().exec(cmd); } // main方法 public static void main(String[] args){ try{ // 调用exec方法执行命令"whoami",并获取进程对象 Process p = exec("whoami"); // 从进程的输入流中读取数据 BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())); String line; // 循环读取输入流的内容并打印到控制台 while((line = input.readLine()) != null){ System.out.println(line); } input.close(); // 等待进程执行结束并获取返回值 int exitCode = p.waitFor(); System.out.println("exit code:"+exitCode); }catch(IOException | InterruptedException e){ // 捕获异常并打印堆栈信息 e.printStackTrace(); } } }
可以看到第二段代码成功执行并返回了结果接着编译一下第二段的代码,现在将java 文件编译成了.class 文件,然后将.class 文件打包成了.jar 文件,在打包成jar文件之前需要先提前准备好另外一个文件:MANIFEST.MF 内容如下
Manifest-Version: 1.0 Main-Class: Cmd
准备好以后开始进行打包工作,因为这里不是一个网站的完整项目目录,所以不采用com.xxx 的方式来打包,而是以零散的方式打包:
第二个代码确定没有问题以后我们将Cmd.jar 这个文件放到 jsp study的www目录下:
然后运行重写的第一段代码,查看运行结果:
可以看到成功获取到第二段代码的命令执行结果了。
java 反射
通过反射的方式,可以获取或调用任意类的任意属性或方法。
反射操作有关 java.lang.Class 对象,所以第一步就是获取Class对象。
主要方法有:
- 类名.class
- Class.forName("类名")
- classloader.loadClass("类名")
public class TestDemo { // 定义一个名为TestDemo的类 public static void main(String[] args){ // 主函数 String classname = "java.lang.Runtime"; // 将字符串变量classname的值改为"java.lang.Runtime" Class<?> class1 = java.lang.Runtime.class; // 获取java.lang.Runtime类的Class对象并赋值给class1 try { Class<?> class2 = Class.forName(classname); // 根据类名动态加载类并返回其对应的Class对象,并赋值给class2 Class<?> class3 = ClassLoader.getSystemClassLoader().loadClass(classname); // 使用系统类加载器加载指定名称的类,并返回对应的Class对象,然后赋值给class3 } catch (ClassNotFoundException e) { // 处理 ClassNotFoundException e.printStackTrace(); // 打印异常信息 } } }
这里展示了3种反射来获取class类的方式,分别是:
- 使用类的字面量
Class<?> class1 = java.lang.Runtime.class;
这种方式是通过类的字面常量来获取该类的 Class 对象。
- 使用Class.forName() 方法
String classname = "java.lang.Runtime"; Class<?> class2 = Class.forName(classname);
使用 Class.forName() 方法,根据类的全限定名动态加载类,并返回对应的 Class 对象。
- 使用类加载器
Class<?> class3 = ClassLoader.getSystemClassLoader().loadClass(classname);
使用类的类加载器,通过类的全限定名称加载类,并返回对应的 Class 对象
反射调用内部类时,要将"." 替换为"$",如com.adan0s.test$TestHello,即 com.adan0s.test 中有一个内部类 TestHello。如:
public class TestDemo { // 定义一个名为TestDemo的类 private class InnerClass{ // 类体 } public static void main(String[] args){ // 主函数 String classname = "java.lang$Runtime"; // 将字符串变量classname的值改为"java.lang.Runtime" Class<?> class1 = java.lang.Runtime.class; // 获取java.lang.Runtime类的Class对象并赋值给class1 try { Class<?> class2 = Class.forName(classname); // 根据类名动态加载类并返回其对应的Class对象,并赋值给class2 Class<?> class3 = ClassLoader.getSystemClassLoader().loadClass(classname); // 使用系统类加载器加载指定名称的类,并返回对应的Class对象,然后赋值给class3 } catch (ClassNotFoundException e) { // 处理 ClassNotFoundException e.printStackTrace(); // 打印异常信息 } } }
Java 反射 -- 获取调用类方法
Class 类有一些方法可以获取当前类的成员方法,包括:
- getMethods() : 获取当前类和父类的所有 public 方法
- getDeclaredMethods() :获取当前类的所有方法,不包括父类
- getMethod() :获取当前类和父类的指定 public 方法
- getDeclaredMethod() :获取当前类的指定方法,不包括父类
import java.lang.reflect.Method; class Person{ String name; Integer age; public Person(String name, Integer age){ this.name = name; this.age = age; } public void setName(String name){ this.name = name; } public void setAge(Integer age){ this.age = age; } public String getName(){ return name; } public Integer getAge(){ return age; } } public class Main { public static void main(String[] args){ // 获取Person 类的 class 对象 Class<?> personClass = Person.class; // 获取当前类和父类的所有方法 Method[] publicMethods = personClass.getMethods(); System.out.println("Public methods:"); for(Method method:publicMethods){ System.out.println(method.getName()); } // 获取当前类的所有方法,不包含父类 Method[] declaredMethods = personClass.getDeclaredMethods(); System.out.println("\nDeclared methods:"); for(Method method:declaredMethods){ System.out.println(method.getName()); } try{ // 获取当前类和父类的指定public 方法 Method getNameMethod = personClass.getMethod("getname"); System.out.println("\nSpecific public method:"); System.out.println(getNameMethod.getName()); // 获取当前类的指定方法,不包含父类 Method setAgeMethod = personClass.getDeclaredMethod("setAge", int.class); System.out.println("\nSpecific declared method:"); System.out.println(setAgeMethod.getName()); }catch(NoSuchMethodException e){ System.out.println("No such method found."); } } }
获取到 java.lang.reflect.Method 对象之后可以利用 Method 类的 invoke 方法进行调用。
invoke(方法实例对象,方法参数值);// 多个参数用逗号隔开
invoke 方法的第一个参数为类实例对象,如果调用的是静态方法,这里可以为null,因为java 中调用静态方法不需要有类实例。
invoke 方法的第二个参数如果当前调用的方法没有参数,那么可以不传入参数,但是如果有参数,就必须严格地一次传入对应的参数类型。
如果方法为私有,就需要通过反射的方式去修改权限,利用Method.setAccessible() 方法:
method.setAccessible(true);
import java.lang.reflect.Method; class Person{ String name; Integer age; public Person(String name, Integer age){ this.name = name; this.age = age; } public void setName(String name){ this.name = name; } public void setAge(Integer age){ this.age = age; } public String getName(){ return name; } public Integer getAge(){ return age; } } public class Main2 { public static void main(String[] args){ try{ // 获取Person 类的Class对象 Class<?> personClass = Person.class; // 获取setName 方法 Method setNameMethod = personClass.getDeclaredMethod("setName",String.class ); // 创建 person 对象 Person person = new Person("John",30); // 调用 setName 方法 setNameMethod.invoke(person,"Alice"); // 打印修改后的属性 System.out.println("Updated name:"+person.getName()); }catch(Exception e){ e.printStackTrace(); } } }
- 获取类的对象
- Class<?> personClass = Person.class;
- 这是获取类的对象,反射的基础条件,和实例化获取对象所不同的是反射获取对象是在运行时动态的获取一个类的信息,而不是创建一个类的实例,通过获取类的Class对象,我们可以在运行时检查类的结构、调用类的方法,访问类的字段等。这种方式通常适用于在运行时动态处理类的结构信息场景,比如编写框架,工具类。
- Class<?> personClass = Person.class;
- 检索类的方法、字段和其他信息
- Method setNameMethod = personClass.getDeclaredMethod("setName",String.class );
- getDeclaredMethod() 方法中传递2个参数分别是:需要获取的方法名,参数类型,所以这里是获取setName方法,setName 方法接受一个String类型的参数。
- Method setNameMethod = personClass.getDeclaredMethod("setName",String.class );
- 创建对象
- Person person = new Person("John",30);
- 这里我们在获取方法之前创建了一个class对象,是因为我们需要通过反射来调用对象的方法,而不是编码时直接调用。而这里将person类实例化最好不要在反射获取类之前,因为我们需要通过反射来获取class对象。
- Person person = new Person("John",30);
- 根据需要对类进行操作
- setNameMethod.invoke(person,"Alice");
- 这里就是通过反射来调用的方法,之前通过对Method类实例化来反射获取到的方法,这里使用invoke 给person对象传递方法参数值:Alice
- setNameMethod.invoke(person,"Alice");
最后尝试对代码编译后输出结果:
Java反射--获取调用成员变量
利用反射可以获取类的所有成员变量,还可以对其进行修改,主要利用的是 Field 方法,包括:
- getFields(); 获取当前类和父类的所有 public 成员
- getDeclaredFields(); 获取当前类所有成员变量,不包括父类
- getField(); 获取当前类和父类的指定 public 成员变量
- getDeclaredField(); 获取当前类的指定成员变量,不包括父类
获取成员变量值: Object object = field.get(类实例对象); 修改成员变量值: field.set(最后的值);
import java.lang.reflect.Field; class Parent{ public int publicFieldParent; private int privateFieldParent; public Parent(int publicFieldParent, int privateFieldParent){ this.publicFieldParent = publicFieldParent; this.privateFieldParent = privateFieldParent; } } class Child extends Parent{ public int publicFieldChild; private int privateFieldChild; public Child(int publicFieldParent, int privateFieldParent,int publicFieldChild,int privateFieldChild ){ super(publicFieldParent, privateFieldParent); this.publicFieldChild = publicFieldChild; this.privateFieldChild = privateFieldChild; } } public class Main { public static void main(String[] args){ try{ // 获取child 类的class对象 Class<?> childClass = Child.class; // 获取当前类和父类的所有 Public 成员变量 Field[] publicField = childClass.getFields(); System.out.println("Public fields:"); for(Field field:publicField){ System.out.println(field.getName()); } // 获取当前类所有成员变量,不包括父类 Field[] declaredFields = childClass.getDeclaredFields(); System.out.println("\nDeclared fields:"); for(Field field:declaredFields){ System.out.println(field.getName()); } // 获取当前类和父类的指定 public 成员变量 Field publicFieldParent = childClass.getField("publicFieldParent"); System.out.println("\nSpecific public field:"); System.out.println(publicFieldParent.getName()); // 获取当前类的指定成员变量,不包括父类 Field privateFieldChild = childClass.getDeclaredField("privateFieldChild"); System.out.println("\nSpecific declared field:"); System.out.println(privateFieldChild.getName()); }catch(NoSuchFieldException e){ System.out.println("Field not found"); } } }
获取思路就是创建一个Field类型的数组对象,然后通过反射获取对应的成员变量并保存到数组中,最后使用for循环遍历数组。
利用反射进行命令执行
利用反射的方式,调用java.lang.Runtime 类的 exec 方法,进行命令执行,下面是无回显的示例代码
import java.lang.reflect.Constructor; import java.lang.reflect.Method; public class RuntimeExample { public static void main(String[] args) throws Exception { // 获取 Runtime 类的 Class 对象 Class<?> runtimeClass = Class.forName("java.lang.Runtime"); // 获取 Runtime 类的默认构造函数并设置为可访问 Constructor<?> constructor = runtimeClass.getDeclaredConstructor(); constructor.setAccessible(true); // 获取 exec 方法 Method execMethod = runtimeClass.getMethod("exec", String.class); // 调用 exec 方法来打开计算器应用程序 execMethod.invoke(constructor.newInstance(), "calc.exe"); } }
利用反射执行命令思路:
- 使用反射动态获取Runtime类的 Class 对象,可通过forName() 方法传递类的全限定名称来实现
- 在获取Runtime 类中的方法时,由于 exec 方法的访问修饰符是 private私有,需要使用 getDeclaredMethod() 方法,并将其访问性设置为true,以便获取私有方法
- 获取到 exec 方法后,可以通过 invoke() 方法来调用该方法,传递一个执行命令作为参数
在这里 newInstance() 是 Java 中用于创建实例的方法。在这个上下文中,constructor.newInstance() 返回的是Runtime 类的一个实例。这个实例是通过constructor 表示的构造函数创建的,因为在前面的代码中,使用了constructor 获取了Runtime 类的默认构造函数。
所以 execMethod.invoke(constructor.newInstance(), "calc.exe"); 中的 newInstance() 是用来创建Runtime类的一个新的实例。然后,我们调用 execMethod.invoke() 方法,将这个新创建的 Runtime 实例作为方法的调用者,并传递 calc.exe 作为方法的参数,来执行命令calc.exe
Runtime 类的构造方法是私有的,无法使用Runtime r = new Runtime() 的方式实例化,示例中是使用反射来完成的,即获取私有构造方法再调用 Constructor.newInstance();
正常情况下,获取Runtime类对象是依靠Runtime.getRuntime() 方法完成的:
public static Runtime getRuntime(){ return currentRuntime; }
这种设置私有构造方法再利用静态方法调用的方式是设计模式的一种,即单例模式。
Runtime 类中常用方法: | |
getRuntime() | 静态方法,用于获取当前运行时的 Runtime 对象 |
exec(String command) | 执行指定的系统命令,返回一个表示进程的 Process 对象 |
totalMemory() | 返回java 虚拟机中的总内存量 |
freeMemory() | 返回 java 虚拟机中的空闲内存量 |
maxMemory() | 放回 java 虚拟机试图使用的最大内存 |
gc() | 运行垃圾回收器 |
exit(int status) | 终止当前运行的java 虚拟机 |
Java 文件操作
java 中有2类文件系统:java.io 和 java.nio ,前者为阻塞模式,后者为非阻塞模式。文件系统抽象出来的对象就是 java.io.FileSystem ,不同的操作系统文件系统不同,这是靠不同操作系统版本的JDK实现其方法来完成的。
即使再不同的操作系统上实现有差别,但最终还是调用 native 方法。
阻塞模式下使用的是 java.io.FileSystem,具体的类为 java.io.FileInputStream ,其他读取文件相关类基本都是对此类的封装。
首先打开一个 File 对象,再创建 FileInputStream 对象,定义缓冲区和二进制输入流对象,循环读取。FileInputStream.read(byte[] bytes)即从此输入流中最多读取bytes.length 个字节的数据到字节数组中,到达文件末尾时会返回 -1。
import java.io.*; public class FileReadTest { public static void main(String[] args) { FileInputStream fileInputStream = null; try { // 实例化File对象 File file = new File("D:/1.txt"); // 实例化输入流对象 fileInputStream = new FileInputStream(file); int a = 0; byte[] bytes = new byte[1024]; // 实例化比特流数组输出对象 ByteArrayOutputStream outByte = new ByteArrayOutputStream(); // 循环遍历字节数据并写入到比特流数组中 while ((a = fileInputStream.read(bytes)) != -1) { outByte.write(bytes, 0, a); } // 将比特流转换成字符串输出 System.out.println(outByte.toString()); } catch (FileNotFoundException e) { System.out.println("File not found"); } catch (IOException e) { e.printStackTrace(); } finally { try { if (fileInputStream != null) { fileInputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
- File file = new File("D:/1.txt");
- 给File类中的构造函数传递参数,File 类的构造函数有多个重载形式,其接受一个文件路径字符串作为参数。
- byte[] bytes = new byte[1024];
- 创建一个1024字节的数组缓冲区用来存储读取的数据,这样做的目的是为了避免重复的I/O读取。
- ByteArrayOutputStream outByte = new ByteArrayOutputStream();
- 这是比特流的数组输出类,用来将写入数组的比特流数据进行输出。
- while ((a = fileInputStream.read(bytes)) != -1)
- 从文件输入流中读取数据,并将读取的字节存储到数组bytes 中,读取的字节数会作为返回值返回,如果已经到达文件末尾,则返回-1.
- 在while 循环中,条件(a = fileInputStream.read(bytes)表示只要读取的字节数不是-1(即还没有到达文件末尾),就继续执行循环体内代码。
- 在循环体中,outByte.write(bytes, 0, a);将读取到的数据写入到ByteArrayOutputStream中。这里的bytes是要写入的字节数组,0是起始偏移量,a是要写入的字节数。
- 最后调用ByteArrayOutputStream 类的toString 方法,该方法用于将字节数组中的数据转换为字符串形式,并返回该字符串
Java 文件名空字节截断漏洞
java 曾出现过一个文件名空字节截断漏洞,于 Java SE7 Update 40 中被修复,在所有跟文件名有关的操作中,都添加了检测空字节的函数,受空字节截断影响的 JDK版本范围:JDK<1.7.40。
截断的具体表现形式:当文件名为test.txt\u0000.jpg ,最终获取到的是test.txt,在文件上传、文件读取及文件删除等常见中都可能存在
import java.io.*; public class FilenameVulnerabilityWithTruncation { // set white.list private static final String[] WHITE_LISTS = {".jpg",".jpeg",".png",".gif"}; public static void main(String[] args){ processUploadFile("D:/uploads/",args[0]); } public static void processUploadFile(String dir,String filename){ try{ // whitelist check if(!isValidExtension(filename)){ System.out.println("File extension is not allowed!"); return; } // null byte check if(filename.contains("\0")){ System.out.println("File name contains null byte!"); return; } File file = new File(dir+filename); // file existence check if(file.exists()){ System.out.println("File "+ filename + " exists."); }else{ System.out.println("File "+filename + " does not exist."); } }catch(Exception e){ e.printStackTrace(); } } private static boolean isValidExtension(String filename){ for(String ext:WHITE_LISTS){ if(filename.endsWith(ext)){ return true; } } return false; } }
这是一个模拟文件上传的案例,在这个案例中通过processUploadedFile 来处理文件上传,由于需要带参数运行,所以我们这里使用cmd来观察一下运行效果:
首先编译成.class 文件:
javac FilenameVulnerabilityWithTruncation.java
接着带参数运行查看一下效果:
可以看到哦上传一个文件名为"shell.jsp\u000.jpg"的文件就可以绕过过滤成功上传。
这是因为在 Java 中,\u0000 表示Unicode 字符中的空字符,因为文件名被视为 shell.jsp\0.jpg 。由于这里的后缀.jpg 在\0之后,程序无法正确识别后缀,因此也就无法进行后缀验证,这样,通过使用空字节截断,成功绕过后缀验证。
Java 文件写入
文件写入使用的类为 java.io.FileOutputStream
import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; public class FileWriteTest{ public static void main(String[] args) throws IOException{ // 创建file 对象,在当前路径写入test.txt 文件 File file = new File("./test.txt"); // 定义文件内容 String content = "Write Success"; // 实例化输出流对象 FileOutputStream fileOutputStream = new FileOutputStream(file); // 调用String 类的getBytes 方法获取字节,然后写入 fileOutputStream.write(content.getBytes(StandardCharsets.UTF_8)); fileOutputStream.flush(); // 写入完成后关闭输出流对象 fileOutputStream.close(); System.out.println("Write to file success!"); } }
Java 命令执行
java 中常用来执行系统命令的类有 java.lang.Runtime;
import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.IOException; public class ExecCommand { public static void main(String[] args) { String cmd = "whoami"; try { // 执行命令并获取输入流 InputStream in = Runtime.getRuntime().exec(cmd).getInputStream(); // 读取输入流的内容并写入 ByteArrayOutputStream ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { byteArrayOutputStream.write(buffer, 0, bytesRead); } // 输出命令执行结果 System.out.println(new String(byteArrayOutputStream.toByteArray())); // 关闭输入流 in.close(); } catch (IOException e) { e.printStackTrace(); } } }
这里是使用的 Java 反射机制来静态调用的。
反射调用可以分为两种方式:静态调用和动态调用
- 反射静态调用(Static Reflection):这种方式是指在编译时确定调用的方法,而不是在运行时。在静态调用中,程序员在编写时已经确定了要调用的方法,并且在编译时就生成了对该方法的调用。在java 中,静态调用通常使用类的名称直接调用静态方法。
Class.forName("java.lang.Runtime");
- 反射动态调用(Dynamic Reflection):这种方式是指在运行时根据程序的状态来确定调用的方法。在动态调用中,程序员在编写代码时并不知道要调用的方法是什么,而是在运行时根据运行时的环境或程序的状态来确定要调用的方法。在 Java 中,通过 java.lang.reflect 包中的类和方法实现动态调用。
Class<?> runtimeClass = Class.forName("java.lang.Runtime"); Constructor<?> constructor = runtimeClass.getDeclaredConstructor(); constructor.setAccessible(true);
以下是动态调用方法原理代码说明:
import java.lang.reflect.Method; import java.util.Scanner; public class DynamicMethodInvocation { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print("Enter a command (sayHello or sayGoodbye): "); String command = scanner.nextLine(); try { // 获取指定类的 Class 对象 Class<?> clazz = DynamicMethodInvocation.class; // 根据命令获取要调用的方法名 String methodName = "say" + command; // 获取方法对象 Method method = clazz.getDeclaredMethod(methodName); // 调用方法 method.invoke(null); } catch (Exception e) { e.printStackTrace(); } } public static void sayHello() { System.out.println("Hello!"); } public static void sayGoodbye() { System.out.println("Goodbye!"); } }
这里通过将用户输入拼接say组成对应的方法名,从而去调用对应的方法。
调用链
- java.lang.UNIXProcess.<init>(UNIXProcess.java:247)
- 在
ProcessImpl
类的start()
方法中,可能涉及到UNIXProcess
类的构造函数的调用。 UNIXProcess
类是用于在 Unix 系统上启动和管理进程的类。
- 在
- java.lang.ProcessImpl.start(ProcessImpl.java:134)
- 在
ProcessBuilder
类的start()
方法中,可能涉及到ProcessImpl
类的start()
方法的调用。 ProcessImpl
类是 Java 平台中实现 Process 接口的类之一,它负责启动和管理进程。
- 在
- java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)
- 在
Runtime
类的exec()
方法中,会创建一个ProcessBuilder
对象,并调用其start()
方法。 ProcessBuilder
类用于创建进程,并提供了更灵活的方法来配置进程的环境、工作目录等。
- 在
- java.lang.Runtime.exec(Runtime.java:620)
- 在
Runtime
类的exec()
方法中,可能涉及到另一个exec()
方法的调用。 - 这里的行号可能对应着
exec(String[] cmdarray)
方法的实现。
- 在
- java.lang.Runtime.exec(Runtime.java:450)
- 在
Runtime
类的exec()
方法中,可能涉及到另一个exec()
方法的调用。 - 这里的行号可能对应着
exec(String command)
方法的实现。
- 在
- java.lang.Runtime.exec(Runtime.java:347)
- 在 JSP 服务方法中,调用了
Runtime
类的exec()
方法。 exec()
方法用于在单独的进程中执行指定的命令。
- 在 JSP 服务方法中,调用了
- org.apache.jsp.runtime_002dexec2_jsp.jspService(runtime_002dexec2_jsp.java:118)
- 这一行显示了在 JSP 文件中的某个方法中的调用。
- 在 JSP 页面中执行了某个服务方法,可能与执行外部进程相关。
Runtime.exec 实际上创建了 ProcessBuilder 对象,调用了PrcoessBuilder.start
ProcessBuilder 是 Java 中用于创建进程的一个类,相比于直接使用Runtime 类的 exec方法,ProcessBuilder 提供了更加灵活和功能更强大的方式来配置和启动进程。
ProcessBuilder 类提供了一系列方法来配置新进程的环境、工作目录、标准输入、输出和错误流等。它的 start() 方法用于启动新进程,并返回一个表示该进程的Process 对象,通过该对象,可以与新进程进行交互。
使用ProcessBuilder
import java.io.ByteArrayOutputStream; import java.io.InputStream; public class MyProcessBuilder { public static void main(String[] args) throws Exception{ // 定义执行命令,类型为字符串 String cmd = "whoami"; // 创建ProcessBuilder 对象,将需要执行的命令以参数的方式传递 ProcessBuilder pb = new ProcessBuilder(cmd); // 启动ProcessBuilder 对象所代表的进程,并返回一个Process 实例 Process process = pb.start(); // 获取进程的输入流用于读取进程的输出。 InputStream in = process.getInputStream(); // 创建字节数组输出流,用于保存从输入流中读取的数据 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 创建一个缓冲区从输入流中读取数据 byte[] b = new byte[1024]; int a = 0; // 循环读取输入流的数直到读取完毕 while((a = in.read(b))!=-1){ // 将读取的字节流数据写到字节数组输出流 byteArrayOutputStream.write(b,0,a); } // 将字节数组输出流中的数据转换为字符串,并输出到控制台 System.out.println(new String(byteArrayOutputStream.toByteArray())); in.close(); } }
以上代码编译执行结果:
由 ProcessBuilder 创建了一个进程,代码会从这个进程中读取数据到代码中,然后需要将从进程中读取到的内容再缓存到一个字节数组输出流中,最后从字节数组输出流中提取出字节流数据,再将字节流数据转换成字符串数据输出。
JDK9 把 UNIXProcess 合并到了 PrcoessImpl 当中,所以可看作同一个。在UNIXProcess 类中有 native 方法 forkAndExec:
private native int forkAndExec(int mode,byte[] helperpath,byte[] prog,byte[] argBlock,int argc,byte[] envBlock,int envc,byte[] dir,byte[] fds,boolean redirectErrorStream) throws IOException;
java 反序列化
Java中可以通过序列化和反序列化实现对象和字节的转换,与PHP序列化一样,保存的是类成员变量和属性值。
只要实现了java.io.Serializable 或 java.io.Externalizable 这两个接口的类都可以被序列化。
java.io.Serializable 是空接口,只用于标识该类可以被序列化,实现此接口的类都应生成一个 serialVersionUID常量,如果未显式声明,序列化时会自动计算。
Java序列化
序列化对象主要依靠 java.io.ObjectOutputStream 类的 writeObject 方法:
import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.util.Arrays; public class Serializable { public static void main(String[] args) { // 创建一个 ByteArrayOutputStream 对象,用于存储序列化后的对象数据 ByteArrayOutputStream byteout = new ByteArrayOutputStream(); try { // 创建一个 SerializationTest 对象,并设置其属性 SerializationTest st = new SerializationTest("adan0s", 21); st.setName("adan0s"); st.setAge(21); // 创建一个 ObjectOutputStream 对象,并将 SerializationTest 对象写入 ByteArrayOutputStream 中 ObjectOutputStream objout = new ObjectOutputStream(byteout); objout.writeObject(st); objout.flush(); objout.close(); // 输出序列化后的字节数组内容 System.out.println(Arrays.toString(byteout.toByteArray())); } catch (Exception e) { e.printStackTrace(); } } } // SerializationTest 类实现了 Serializable 接口,可以被序列化 class SerializationTest implements java.io.Serializable { private static final long serialVersionUID = 1L; private String name; private int age; // 构造函数,用于初始化对象的属性 public SerializationTest(String name, int age) { this.name = name; this.age = age; } // 设置姓名 public void setName(String name) { this.name = name; } // 获取姓名 public String getName() { return name; } // 设置年龄 public void setAge(int age) { this.age = age; } // 获取年龄 public int getAge() { return age; } // 重写 toString 方法,用于返回对象的字符串表示形式 @Override public String toString() { return "SerializatonTest [name =" + name + ", age =" + age + "]"; } }
以上代码编译运行结果:
因为这里是将数据序列化成了字节数组,所以展示出来的是数组结果
Java 反序列化
反序列化时需要满足:
- 被反序列化的类必须存在
- serialVersionUID 值必须一致
在反序列化的时候,不会调用该类的构造方法,因为反序列化时使用了 sun.reflect.ReflectionFactory.newConstructorForSerialization 创建了新的构造方法,使用此构造方法创建了实例
反序列化时主要用到 java.io.ObjectInputStream 类的 readObject 方法:
import java.io.ByteArrayInputStream; import java.io.ObjectInputStream; public class UnSerializable { public static void main(String[] args){ byte[] seridata = new byte[]{-84, -19, 0, 5, 115, 114, 0, 17, 83, 101, 114, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, 84, 101, 115, 116, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 2, 73, 0, 3, 97, 103, 101, 76, 0, 4, 110, 97, 109, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 120, 112, 0, 0, 0, 21, 116, 0, 6, 97, 100, 97, 110, 48, 115}; ByteArrayInputStream bytein = new ByteArrayInputStream(seridata); try{ ObjectInputStream in = new ObjectInputStream(bytein); SerializationTest st = (SerializationTest) in.readObject(); System.out.println("Name: "+ st.getName()); System.out.println("Age: "+st.getAge()); }catch(Exception e){ e.printStackTrace(); } } } class SerializationTest implements java.io.Serializable { private static final long serialVersionUID = 1L; private String name; private int age; // 构造函数,用于初始化对象的属性 public SerializationTest(String name, int age) { this.name = name; this.age = age; } // 设置姓名 public void setName(String name) { this.name = name; } // 获取姓名 public String getName() { return name; } // 设置年龄 public void setAge(int age) { this.age = age; } // 获取年龄 public int getAge() { return age; } // 重写 toString 方法,用于返回对象的字符串表示形式 @Override public String toString() { return "SerializatonTest [name =" + name + ", age =" + age + "]"; } }
首先创建一个字节数组对象seridata,将序列化后的字节数组赋值给seridate。接着创建了一个ByteArrayInputStream 类型的对象bytein对象,它允许将字节数组作为输入流,然后创建一个ObjectInputStream 对象,将字节数组输入流传递进去,从而允许从字节数组输入流中读取内容,最后通过 readObject(); 方法读取字节流对象并将其强制转换成SerializationTest 类对象。
Serializable 接口的序列化
Java 序列化是一种将对象转换为字节流的过程,以便可以将对象保存到磁盘上,将其传输到网络上,或者将其存储在内存中,以后再进行反序列化,将字节流重新转换为对象。
序列化在 Java 中是通过 java.io.Serializable 接口来实现的,该接口没有任何方法,只是一个标记接口,用于标识类可以被序列化。
当你序列化对象时,你把它包装成一个特殊文件,可以保存、传输或存储。反序列化则是打开这个文件,读取序列化的数据,然后将其还原为对象,以便在程序中使用。
序列化是一种用于保存、传输和还原对象的方法,它使得对象可以在不同的计算机之间移动和共享,这对于分布式系统、数据存储和跨平台通信非常有用。
以下是 Java 序列化的基本概念和用法:
实现 Serializable 接口: 要使一个类可序列化,需要让该类实现 java.io.Serializable 接口,这告诉 Java 编译器这个类可以被序列化,例如:
import java.io.Serializable;; public class Se implements Serializable { }
序列化对象: 使用 ObjectOutputStream 类来将对象序列化为字节流,以下是一个简单的实例:
import java.io.*; // 在类级别添加注释,说明这是一个演示Java序列化的类 public class SerializationDemo { // 在方法级别添加注释,说明这是主方法 public static void main(String[] args) { MyClass obj = new MyClass(); try { FileOutputStream fileOut = new FileOutputStream("object.ser"); // 创建文件输出流以将对象序列化到文件 ObjectOutputStream out = new ObjectOutputStream(fileOut); // 创建对象输出流以将对象写入文件 out.writeObject(obj); // 将对象写入输出流 out.close(); // 关闭对象输出流 fileOut.close(); // 关闭文件输出流 System.out.println("对象已序列化并保存为object.ser"); // 打印提示信息 } catch (IOException e) { e.printStackTrace(); // 捕获并打印I/O异常 } } } class MyClass implements Serializable { // 在这里定义类的成员变量和方法 }
这里将流生成一个文件,需要用二进制处理软件打开,代码执行后效果:
使用二进制工具打开后可以看到:
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)