文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

大数据安全

机器学习

基础学习

Python

Python基础

Python安全

Java

Java基础

算法

Leetcode

随笔

经验

技术

 2021-01-22   2k

Java基础学习之反射和注解

Reference: https://liaoxuefeng.com

0x01 反射

Class类

JVM每加载一种class,就为其创建一个Class实例,并关联起来。注意:这里的Class类型是一个名叫Classclass,形如:

1
2
3
public final class Class {
private Class() {}
}

以String类为例,当JVM加载String类的时候,首先会读取String.class到内存,然后,为String类创建一个Class实例并关联起来:

1
Class cls = new Class(String);

JVM持有的每个Class实例都指向一个数据类型(classinterface):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌───────────────────────────┐
│ Class Instance │──────> String
├───────────────────────────┤
│name = "java.lang.String" │
└───────────────────────────┘
┌───────────────────────────┐
│ Class Instance │──────> Random
├───────────────────────────┤
│name = "java.util.Random" │
└───────────────────────────┘
┌───────────────────────────┐
│ Class Instance │──────> Runnable
├───────────────────────────┤
│name = "java.lang.Runnable"│
└───────────────────────────┘

一个Class实例包含了该class的所有完整信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌───────────────────────────┐
│ Class Instance │──────> String
├───────────────────────────┤
│name = "java.lang.String" │
├───────────────────────────┤
│package = "java.lang" │
├───────────────────────────┤
│super = "java.lang.Object" │
├───────────────────────────┤
│interface = CharSequence...│
├───────────────────────────┤
│field = value[],hash,... │
├───────────────────────────┤
│method = indexOf()... │
└───────────────────────────┘

由于JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息。

这种通过Class实例获取class信息的方法称为反射(Reflection)。

获取一个classClass实例有三种方法:

方法一:直接通过一个class的静态变量class获取:

1
Class cls = String.class;

方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取:

1
2
String s = "Hello";
Class cls = s.getClass();

方法三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:

1
Class cls = Class.forName("java.lang.String");

访问字段

Class类提供了以下几个方法来获取字段:

  • Field getField(name):根据字段名获取某个public的field(包括父类)
  • Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
  • Field[] getFields():获取所有public的field(包括父类)
  • Field[] getDeclaredFields():获取当前类的所有field(不包括父类)

一个Field对象包含了一个字段的所有信息:

  • getName():返回字段名称,例如,"name"
  • getType():返回字段类型,也是一个Class实例,例如,String.class
  • getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的含义。

还是以上面这段代码为例:

获取字段值

利用反射拿到字段的一个Field实例之后,可以拿到一个实例对应的该字段的值:

如果不调用Field.setAccessible(true),就会抛出一个IllegalAccessException的错误,这是因为name字段被定义成private

设置字段值

除了使用get方法获取字段值,还可以用set方法获取字段值:

同样,修改非public字段,需要首先调用setAccessible(true)

调用方法

Class类提供了以下几个方法来获取Method

  • Method getMethod(name, Class...):获取某个publicMethod(包括父类)
  • Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
  • Method[] getMethods():获取所有publicMethod(包括父类)
  • Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)

一个Method对象包含一个方法的所有信息:

  • getName():返回方法名称,例如:"getScore"
  • getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class
  • getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
  • getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。

当我们获取到一个Method对象时,就可以对它进行调用。我们以下面的代码为例:

1
2
String s = "Hello world";
String r = s.substring(6); // "world"

如果用反射来调用substring方法:

调用静态方法

如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke方法传入的第一个参数永远为null。以Integer.parseInt(String)为例:

调用非public方法

和Field类似,对于非public方法,我们虽然可以通过Class.getDeclaredMethod()获取该方法实例,但直接对其调用将得到一个IllegalAccessException。为了调用非public方法,我们通过Method.setAccessible(true)允许其调用:

多态

一个Person类定义了hello()方法,并且它的子类Student也覆写了hello()方法,那么,从Person.class获取的Method,作用于Student实例时,同样会遵循多态原则,实际调用的是Student类的hello()方法。

调用构造方法

Java的反射API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。

通过Class实例获取Constructor的方法如下:

  • getConstructor(Class...):获取某个publicConstructor
  • getDeclaredConstructor(Class...):获取某个Constructor
  • getConstructors():获取所有publicConstructor
  • getDeclaredConstructors():获取所有Constructor

获取继承关系

获取父类class

获取interface

由于一个类可能实现一个或多个接口,通过Class我们就可以查询到实现的接口类型。例如,查询String实现的接口:

动态代理

https://www.jianshu.com/p/95970b089360

在运行期动态创建一个interface实例的方法如下:

  1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;
  2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
    1. 使用的ClassLoader,通常就是接口类的ClassLoader
    2. 需要实现的接口数组,至少需要传入一个接口进去;
    3. 用来处理接口方法调用的InvocationHandler实例。
  3. 将返回的Object强制转型为接口。

0x02 注解

常用的元注解

@Target

使用@Target可以定义Annotation能够被应用于源码的哪些位置:

  • 类或接口:ElementType.TYPE
  • 字段:ElementType.FIELD
  • 方法:ElementType.METHOD
  • 构造方法:ElementType.CONSTRUCTOR
  • 方法参数:ElementType.PARAMETER

@Retention

元注解@Retention定义了Annotation的生命周期:

  • 仅编译期:RetentionPolicy.SOURCE
  • 仅class文件:RetentionPolicy.CLASS
  • 运行期:RetentionPolicy.RUNTIME

如果@Retention不存在,则该Annotation默认为CLASS

如何定义Annotation

我们总结一下定义Annotation的步骤:

第一步,用@interface定义注解:

1
2
public @interface Report {
}

第二步,添加参数、默认值:

1
2
3
4
5
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}

把最常用的参数定义为value(),推荐所有参数都尽量设置默认值。

第三步,用元注解配置注解:

1
2
3
4
5
6
7
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}

其中,必须设置@Target@Retention@Retention一般设置为RUNTIME,因为我们自定义的注解通常要求在运行期读取。

使用注解

Person.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Person {

@Range(min = 1, max = 20)
public String name;

@Range(max = 10)
public String city;

@Range(min = 1, max = 100)
public int age;

public Person(String name, String city, int age) {
this.name = name;
this.city = city;
this.age = age;
}

@Override
public String toString() {
return String.format("{Person: name=%s, city=%s, age=%d}", name, city, age);
}
}

Range.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {

int min() default 0;

int max() default 255;

}

Main.java

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
import java.lang.reflect.Field;

/**
* Learn Java from https://www.liaoxuefeng.com/
*
* @author liaoxuefeng
*/
public class Main {

public static void main(String[] args) throws Exception {
Person p1 = new Person("Bob", "Beijing", 20);
Person p2 = new Person("", "Shanghai", 20);
Person p3 = new Person("Alice", "Shanghai", 199);
for (Person p : new Person[] { p1, p2, p3 }) {
try {
check(p);
System.out.println("Person " + p + " checked ok.");
} catch (IllegalArgumentException e) {
System.out.println("Person " + p + " checked failed: " + e);
}
}
}

static void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
for (Field field : person.getClass().getFields()) {
Range range = field.getAnnotation(Range.class);
if (range != null) {
Object value = field.get(person);
// TODO:
if (value instanceof String){
String s = (String) value;
if (s.length() < range.min() || s.length() > range.max()){
throw new IllegalArgumentException("Invalid name" + field.getName());
}
}else if (value instanceof Integer){
Integer i = (Integer) value;
if (i < range.min() || i > range.max()){
throw new IllegalArgumentException("Invalid age" + field.getName());
}
}
}
}
}
}

输出:

1
2
3
Person {Person: name=Bob, city=Beijing, age=20} checked ok.
Person {Person: name=, city=Shanghai, age=20} checked failed: java.lang.IllegalArgumentException: Invalid namename
Person {Person: name=Alice, city=Shanghai, age=199} checked failed: java.lang.IllegalArgumentException: Invalid ageage
Copyright © ca01h 2019-2020 | 本站总访问量