Java反射 & 注解学习总结

Java反射 & 注解学习总结

反射

概念 & 作用

反射(reflection):运行时查看元数据,反观察、修改程序内部结构。

  • 元数据(MetaData),指运行时的数据,如模块、类、函数、注解、源代码

  • 运行时修改代码:一般指修改Bytecode,byte[],用工具Javasist

【注意】如果在运行时分析泛型类的实例,将不会得到太多的信息,因为它们已经被擦除了。

反射实例

常用方法

  • Class.forName():通过类全限定名获取一个class,要传入全路径
  • getClass():获取一个对象的运行时类
  • [class].getConstructor().newInstance():通过构造器创建一个类
  • [class].getDeclaredFields():获取所有属性,包括private
  • [class]..getDeclaredMethods():获取所有方法
  • [class].getClassLoader():获取类加载器
  • method.invoke([class]):调用一个类中的所有方法

获取类的属性、方法

实例:

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
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

// 创建一个类
var list = Arrays.asList("List", "Tree", "Array");
var randomGen = new RandomStringGenerator(list);
var clazz = randomGen.getClass();

/**
* 获取类名
*/
System.out.println("class name:" + clazz.getName());
System.out.println();

/**
* 获取所有属性
* getField() 只能获取public的,包括从父类继承来的字段
* getDeclaredField() 可以获取当前类所有的字段,包括private的,但是不能获取继承来的字段
*/
var fields = clazz.getDeclaredFields();
System.out.println("All fields are as below: ");
for(var field : fields)
{
System.out.println("field name: " + field.getName());
System.out.println("field type: " + field.getGenericType());
System.out.println();
}

/**
* 获取所有方法
*/
var methods = clazz.getDeclaredMethods();
System.out.println("All methods are as below: ");
for(var method : methods)
{
System.out.println("method sign: "
+ Modifier.toString(method.getModifiers()) + " "
+ method.getReturnType() + " "
+ method.getName());
}
}

反射创建对象 & 调用方法

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

var list = Arrays.asList("List", "Tree", "Array");

// 通过反射创建实例,带参数的构造器
var clazz = (RandomStringGenerator)Class.forName("com.javacoding.collection.RandomStringGenerator")
.getConstructor(List.class).newInstance(list);

// 获取main方法,并调用
var methods = Arrays.stream(clazz.getClass().getMethods())
.filter(x -> x.getName().equals("main"))
.collect(Collectors.toList());
// 设置传参类型
methods.get(0).invoke(null, new Object[]{new String[]{"test1","test2","test3"}});

// 调用main方法不能用如下的方法:
//methods.get(0).invoke(String[].class);

}

这里注意一下 main 方法的调用

java1.4是没有可变参数的,即Method.invoke(Obeject objecr ,Object obj[]{“xx”,“yy”});用数组传递多个参数 。
java1.5有了可变参数,即Method.invoke(Objet objecr,Object…args);用可变参数传递多个参数。

由于兼容性问题,在JDK1.4没有引入可变参数Object…类型,所以使用数组来表示,invoke函数接收到String数组后会进行拆分,得到两个String变量,但是没有main函数接受 两个String值,所以不行。

而且只要参数是new Object[]{“xxx”,”yyy”},javac 为了兼容1.4,只把它当作 jdk1.4 的语法进行理解,而不把它当作 jdk1.5 的语法解释,也就是说不能传递new String[]{};。

解决方法:
(1)、mainMethod.invoke(null,new Object[]{new String[]{“20190212”}});该方法还是会按1.4处理,会将参数打散,但没关系,里面就是对的参数new String[]{“xxx”}。

(2)、mainMethod.invoke(null,(Object)new String[]{“20190212”}); 不会打散,因为强转为Object了,编译器会认为这不是一个数组,此时是一个完整的参数。但注意他本质还是一个数组,作用仅仅是骗过编译器而已。

实际应用 - AOP

  • 实现AOP,面向切面编程(即Aspect Oriented Programming)的框架

    • 关注点分离原则(SoC,即Seperation of Concern),强调把程序分成多个部分,每个部分负责独立的功能

    • 程序有一个主关注点 + 多个其他关注点

      • 什么时候触发:before、after、around
      • 触发什么:调用对象

      举例:订单系统中的核心能力(下单、支付);其他能力(日志、通知、抽奖等等)

      image.png

    • 实现1:代理模式(Java中的Proxy类)

      • 代理的作用,相当于是把多个 Aspect 和 一个核心方法,在运行时融合起来
      • image.png
    • 实现2:函数式:高阶编程

      • f:核心方法
      • g:方面包装方法
      • x:核心输入方法
      • 最后调用方法:h = fg(x)
    • 实现3:Javassist库动态添加方法

      • AspectJ等框架会使用
  • 实现注解

手写AOP实例

Java 的代理 Proxy

image.png

Proxy.newInstance(classloader, interfaces, InvocationHandler);

  • classloader:用于创建InvocationHander和代理对象的ClassLoader
  • interfaces: 代理哪些接口
  • InvocationHandler: 代理方法触发后的实际执行者

本质上,Proxy是把多个模块化的 Aspect 和 一个核心方法,在运行时融合起来,当成是一个综合实现多个功能的类。

1.创建业务接口IOrder

1
2
3
4
5
6
7
8
9
/**
* 订单支付接口
*/
public interface IOrder {

void pay() throws InterruptedException;

void show();
}

2.创建业务实现类Order

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 订单支付实现
*/
public class Order implements IOrder{

int state = 0;

@Override
public void pay() throws InterruptedException {

// 模拟支付时间间隔
Thread.sleep(50);

// 支付完毕
this.state = 1;
}

@Override
public void show() {
System.out.println("order state:" + state);
}
}

3.创建切面接口Aspect,内置静态方法,用于创建Proxy

该类只能针对接口做的代理

Proxy.newInstance(classloader, interfaces, InvocationHandler);

  • classloader:用于创建InvocationHandler和代理对象的ClassLoader
  • interfaces: 代理哪些接口
  • InvocationHandler: 代理方法触发后的实际执行者

思考:

  • 为什么需要InvocationHandler?

    • 用来衔接、融合代理对象和Aspect对象
  • 为什么需要传入Interfaces?

    • 接口可能会有多个,被代理的接口会被拦截
  • 为什么需要ClassLoader?

    • 可能会有多个ClassLoader做不同的事情,必须要指定

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
/**
* AOP接口
*/
public interface Aspect {

void before();

void after();

// 提供静态方法
static <T> T getProxy(Class<T> cls, String... aspects) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

var aspectInsts = Arrays.stream(aspects).map(name -> Try.ofFailable(() -> {
var clazz = Class.forName(name);
return (Aspect) clazz.getConstructor().newInstance();
}))
.filter(aspect -> aspect.isSuccess())
.collect(Collectors.toList());

var inst = cls.getConstructor().newInstance();
return (T) Proxy.newProxyInstance(cls.getClassLoader(),
cls.getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

for (var aspect : aspectInsts) {
aspect.get().before();
}

var result = method.invoke(inst);

for (var aspect : aspectInsts) {
aspect.get().after();
}

return result;
}
});
}
}

4.创建一个切面实例,计算方法的执行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* AOP实现:用于监控程序运行时间
*/
public class TimeUsageAspect implements Aspect{

long start;

@Override
public void before() {
start = System.currentTimeMillis();
}

@Override
public void after() {
var usage = System.currentTimeMillis() - start;

System.out.format("time usage : %dms\n",usage);
}
}

5.新建测试类

1
2
3
4
5
6
7
8
9
10
11
12
/**
* AOP测试实例
*/
public class ProxyExampleTest {

@Test
public void test_Proxy() throws InterruptedException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
IOrder order = Aspect.getProxy(Order.class,"com.javacoding.proxy.TimeUsageAspect");
order.pay();
order.show();
}
}

【注意1】静态方法返回值为泛型的时候为什么static后要加<T>?

这个是java声明泛型方法的固定格式,在方法的返回值声明之前的位置,定义该方法所拥有的泛型标识符,个数可以是多个,比如

1
public static<T1,T2,T3> Response<T1> test(T2 t2,T3 t3){  }

这样,在方法的返回值或者入参的地方,就可以使用“T”这个泛型。
泛型标识符的名字可以是任意符合java规范的变量名,比如Map就使用<K,V>作为它的key和value的泛型标识符。

【注意2】String...为可变参数,可以传入一个或多个String,在编译时会自动封装为String数组

注解

注解就像一个标签,是贴在程序代码上供另一个程序读取的。其实也可以理解为一些外部的通用功能,在运行时融合到业务类中,但是传递参数的时候变得更加容易。本质还是为了解耦和提供类能力的扩展。

一般而言,注解多由第三方库提供,通过动态代理proxy和反射实现,或者是Javassist动态修改类来达到目的。

基本流程:程序读取注解 -> 找到注解 -> 找到类并实例化

image.png

基本使用方法

注解声明,此时注解没有作用域,仅仅是一个定义。

按如下的定义,可以这样调用注解@Aspect(name = UsageAspect.class) ,相当于是给name()方法赋值,传入了一个名为 UsageAspect 的类,这个类可以通过反射获取。

同理,@Aspect(value = "测试")可以给value()方法传入一个字符串。这些属性可以通过反射获取

1
2
3
4
5
6
7
8
public @interface Aspect {

// 接收一个class类
public Class name();

// 接受一个值
//public String value();
}

【注意】本质上@interface相当于继承了一个Annotation接口,而注解里的方法被编译为Abstract方法

元注解

@Retention

声明注解的存活周期

一般开发都是通过反射获取注解,故使用@Retention(RetentionPolicy.RUNTIME)

  • Retention英文意思有保留、保持的意思,它表示注解存在阶段是保留在源码(编译期),字节码(类加载)或者运行期(JVM中运行)。在@Retention注解中使用枚举RetentionPolicy来表示注解保留时期
  • @Retention(RetentionPolicy.SOURCE),注解仅存在于源码中,在class字节码文件中不包含
  • @Retention(RetentionPolicy.CLASS), 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得
  • @Retention(RetentionPolicy.RUNTIME), 注解会在class字节码文件中存在,在运行时可以通过反射获取到

@Target

声明注解的作用范围

常用@Target(ElementType.TYPE)

  • Target的英文意思是目标,这也很容易理解,使用@Target元注解表示我们的注解作用的范围就比较具体了,可以是类,方法,方法参数变量等,同样也是通过枚举类ElementType表达作用类型
  • @Target(ElementType.TYPE) 作用接口、类、枚举、注解
  • @Target(ElementType.FIELD) 作用属性字段、枚举的常量
  • @Target(ElementType.METHOD) 作用方法
  • @Target(ElementType.PARAMETER) 作用方法参数
  • @Target(ElementType.CONSTRUCTOR) 作用构造函数
  • @Target(ElementType.LOCAL_VARIABLE)作用局部变量
  • @Target(ElementType.ANNOTATION_TYPE)作用于注解(@Retention注解中就使用该属性)
  • @Target(ElementType.PACKAGE) 作用于包

其他的3个元注解,用到再写

  • @Document
  • @Inherited
  • @Repeatable

注解实例

实例:注解实现AOP

1.定义注解

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)  // 注解存活时间
@Target(ElementType.TYPE) // 指可以用于类型上
public @interface Aspect {

public Class type();
}

2.定义切面接口

1
2
3
4
5
6
public interface IAspect {

void before();
void after();
}

3.定义切面实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* AOP实现:用于监控程序运行时间
*/
public class TimeUsageAspect implements IAspect {

long start;

@Override
public void before() {
start = System.currentTimeMillis();
}

@Override
public void after() {
var usage = System.currentTimeMillis() - start;

System.out.format("time usage : %dms\n",usage);
}
}

4.定义业务接口

1
2
3
4
5
6
7
8
9
/**
* 订单支付接口
*/
public interface IOrder {

void pay() throws InterruptedException;

void show();
}

5.定义业务实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 订单支付实现
*/
@Aspect(type=TimeUsageAspect.class)
public class Order implements IOrder {

int state = 0;

@Override
public void pay() throws InterruptedException {

// 模拟支付时间间隔
Thread.sleep(50);

// 支付完毕
this.state = 1;
}

@Override
public void show() {
System.out.println("order state:" + state);
}
}

6.定义工厂方法 & 单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 订单支付实现
*/
@Aspect(type=TimeUsageAspect.class)
public class Order implements IOrder {

int state = 0;

@Override
public void pay() throws InterruptedException {

// 模拟支付时间间隔
Thread.sleep(50);

// 支付完毕
this.state = 1;
}

@Override
public void show() {
System.out.println("order state:" + state);
}
}

实例:注解实现属性注入

1.定义一个注解

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Injection {

public String value();
}

2.定义一个实体类

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

@Injection(value = "Ashe")
private String name;

@Injection(value = "600")
private String range;

@Injection(value = "100")
private String DPS;

@Override
public String toString() {
return "Hero{" +
"name='" + name + '\'' +
", range='" + range + '\'' +
", DPS='" + DPS + '\'' +
'}';
}
}

3.定义一个静态工厂,负责属性注入

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

public static <T> Object inject(Class<T> cls,Class<? extends Injection> annotation) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
var fields = cls.getDeclaredFields();

var obj = cls.getConstructor().newInstance();

for (var field : fields) {
if (field.isAnnotationPresent(annotation)) {
// 允许访问私有属性
field.setAccessible(true);

var value = field.getAnnotation(annotation).value();
field.set(obj, value);

}
}

System.out.println("属性注入完成!");

return obj;
}

}

4.测试类

1
2
3
4
5
6
7
8
9
public class TestInjection {

@Test
public void test_injection() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
var obj = (Hero) InjectionFactory.inject(Hero.class,Injection.class);

System.out.println(obj.toString());
}
}

CGLib/Javassist 实现动态修改字节码

image.png

元编程

#TODO# 元编程

0%