13.java程序员必知必会类库之字节码库

前言

我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是java字节码库

Java代码编译完会生成.class文件,就是一堆字节码。JVM(准确说是JIT)会解释执行这些字节码(转换为机器码并执行),由于字节码的解释执行是在运行时进行的,那我们能否手工编写字节码,再由JVM执行呢?答案是肯定的。而字节码库就提供了一些方便的方法,让我们通过这些方法生成字节码

1 CGLib

1.1 简介

CGLIB是一个强大的高性能的代码生成包。是一个功能强大、高性能、高质量的字节码操作库,主要用于在运行时扩展 Java 类或者根据接口生成对象

它广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截)。最流行的OR Mapping工具hibernate也使用CGLIB来代理单端single-ended(多对一和一对一)关联(对集合的延迟抓取,是采用其他机制实现的)。EasyMock和jMock是通过使用模仿(mock)对象来测试java代码的包。它们都通过使用CGLIB来为那些没有接口的类创建模仿(mock)对象。

CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。除了CGLIB包,脚本语言例如Groovy和BeanShell,也是使用ASM来生成java的字节码。当然不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。

1.2 使用

pom坐标引入

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.5</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.9</version>
</dependency>

在CgLib中,有个很核心的类叫Enhancer, Enhancer可以生成被代理类的子类,并且拦截所有方法的调用,也就是通常所说的增强。需要注意,Enhancer 不能增强构造函数,也不能增强被 final 修饰的类,或者被 static 和 final 修饰的方法。

Enhancer 的使用分为两步,1. 传入目标类型;2. 设置回调。支持不同类型回调是 cglib 最强大的地方。在 Enhancer 创建一个代理类之后所实现的行为要通过这些回调来实现。

下面主要就Enhancer的一些回调函数,做详细介绍,在测试案例中,了解cglib使用方式。

1.2.1 FixedValue

1.2.1.1 目标类(后面测试案例复用)
public class User {
    public User sayHello(String name) {
        System.out.println("你好,+" + name + "我是目标对象");
        return null;
    }

    public User sayHello2(String name) {
        System.out.println("你好2,+" + name + "我是目标对象");
        return null;
    }
    public String returnString(String name) {
        System.out.println("你好3,+" + name + "我是目标对象,返回字符串格式");
        return null;
    }
}
1.2.1.2 测试方法
@Test
public void testEnhancer(){

    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(User.class);
    enhancer.setCallback(new FixedValue() {
        @Override
        public Object loadObject() throws Exception {
            System.out.println("Hello cglib");
            User user = new User();
            System.out.println("vvvv"+user.hashCode());
            return user ;
        }
    });
    User proxy = (User) enhancer.create();
    System.out.println("---------");
    proxy.sayHello("cglib");
    // 会打印 Hello cglib
    System.out.println("---------");
    proxy.sayHello2("wewewe");
}
1.2.1.3 运行结果
---------
Hello cglib
vvvv1510067370
---------
Hello cglib
vvvv1908923184
1.2.1.4 结果分析

可以看到:实现FixedValue接口,该callback要求实现一个loadobject方法,只不过需要注意的是该loadobject方法相当于重写了被代理类的相应方法,因为在被代理之后,FixedValue callback只会调用loadobject,而不会再调用代理目标类的相应方法!

另外,需要留意,第二次调用的方法名换了,实际上还是调用的重写的loadObjec方法。,除了 static 和 final 类型的方法,其他所有的方法都会执行上面的代码,如果调用的方法返回值和loadObject 方法返回类型不一样,会报错类强转错误

1.2.2 InvocationHandler

1.2.2.1 测试方法
@Test
public void testInvocationHandler() {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(User.class);
    enhancer.setCallback(new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] objects) throws Throwable {
            if (method.getReturnType() == String.class) {
                System.out.println("Hello cglib");
                return "Hello cglib";
            } else {
                System.out.println("Invoke other method");
                return method.invoke(proxy, objects); 
                // 这里可能会出现无限循环
            }
        }
    });
    User proxy = (User) enhancer.create();
    proxy.returnString("我是呆子");
    proxy.sayHello("老张");
}
1.2.2.2 运行结果
Hello cglib
Invoke other method
Invoke other method
Invoke other method
Invoke other method
。。。。。。。无限循环中
1.2.2.3 结果分析

这个代码并不会正常返回结果,而是进入无限循环,这是因为这个代理对象的每一个可以被代理的方法都被代理了,在调用被代理了的方法时,会重复进入到 InvocationHandler#invoke 这个方法中,然后进入死循环。
需要实现InvocationHandler接口,实现invoke对象,该拦截传入了proxy对象,用于自定义实现,与MethodInterceptor相似,慎用method的invoke方法。切忌不要造成循环调用

1.2.3 MethodInterceptor

1.2.3.1 测试方法
@Test
public void testMethodInterceptor(){
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(User.class);
    enhancer.setCallback(new MethodInterceptor() {
        @Override
        public Object intercept(Object o, Method method, Object[] params, MethodProxy methodProxy) throws Throwable {
            if (method.getReturnType() == String.class) {
                System.out.println("Hello cglib");
                return "Hello cglib";
            } else {
                // 对上面无限问题的改善
                return methodProxy.invokeSuper(o, params); 
            }
        }
    });
    User proxy = (User) enhancer.create();
    proxy.returnString("我是呆子");
    proxy.sayHello("laowang");
}
1.2.3.2 运行结果
Hello cglib
你好,laowang我是目标对象
1.2.3.3 结果分析

在 MethodInterceptor 中,有一个 MethodProxy 参数,这个就可以用来执行父类方法。实现MethodInterceptor的intercept,实现被代理对象的逻辑植入,也是最常用的callback。

1.2.4 LazyLoader

1.2.4.1 测试方法
  1. 待懒加载类
package com.wanlong.cglib;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class PropertyBean {
    private String propertyName;
    private int propertyValue;

}
  1. 加载类(属性包含上面待懒加载类)
package com.wanlong.cglib;

import lombok.Getter;
import lombok.Setter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.LazyLoader;

@Getter
@Setter
public class LoaderBean {
    private String loaderName;
    private int loaderValue;
    private PropertyBean propertyBean;

    public LoaderBean() {
        this.loaderName = "loaderNameA";
        this.loaderValue = 123;
        this.propertyBean = createPropertyBean();
    }

    protected PropertyBean createPropertyBean() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(PropertyBean.class);
        enhancer.setCallback(new LazyLoader() {
            @Override
            public Object loadObject() throws Exception {
                System.out.println("LazyLoader loadObject() ...");
                PropertyBean bean = new PropertyBean();
                bean.setPropertyName("lazy-load object propertyName!");
                bean.setPropertyValue(11);
                return bean;
            }
        });
        PropertyBean o = (PropertyBean) enhancer.create();
        return o;
    }

}
  1. 测试
package com.wanlong.cglib;

import com.wanlong.cglib.LoaderBean;
import com.wanlong.cglib.PropertyBean;
import org.junit.Test;

/**
 * @author wanlong
 * @version 1.0
 * @description:
 * @date 2023/4/27 18:43
 */
public class TestEnhancer2 {

    @Test
    public void testLazyLoader() {

        LoaderBean loader = new LoaderBean();
        System.out.println(loader.getLoaderName());
        System.out.println(loader.getLoaderValue());
        PropertyBean propertyBean = loader.getPropertyBean();
        //访问延迟加载对象
        System.out.println("--------------------");
        System.out.println(propertyBean.getPropertyName());
        System.out.println(propertyBean.getPropertyValue());
        System.out.println("after-------------");
        //当再次访问延迟加载对象时,就不会再执行回调了
        System.out.println(propertyBean.getPropertyName());
    }
}
1.2.4.2 运行结果
loaderNameA
123
--------------------
LazyLoader loadObject() ...
lazy-load object propertyName!
11
after-------------
lazy-load object propertyName!
1.2.4.3 结果分析

第一次获取property bean的属性时,会触发代理类回调方法
第二次再获取property bean的属性时,就直接返回属性值而不会再次触发代理类回调方法了。
可见,延迟加载原理
需要延迟加载的对象添加代理,在获取该对象属性时先通过代理类回调方法进行对象初始化
不需要加载该对象时,只要不去获取该对象内属性,该对象就不会被初始化了(在CGLib的实现中只要去访问该对象内属性的getter方法,就会自动触发代理类回调)。

1.2.5 Dispatcher

1.2.5.1 测试方法
  1. 替换上面案例LoaderBean为LoaderBeanDispatcher
package com.wanlong.cglib;

import lombok.Getter;
import lombok.Setter;
import net.sf.cglib.proxy.Dispatcher;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.LazyLoader;

@Getter
@Setter
public class LoaderBeanDispatcher {
    private String loaderName;
    private int loaderValue;
    private PropertyBean propertyBean;

    public LoaderBeanDispatcher() {
        this.loaderName = "loaderNameA";
        this.loaderValue = 123;
        this.propertyBean = createPropertyBean();
    }

    protected PropertyBean createPropertyBean() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(PropertyBean.class);
        enhancer.setCallback(new Dispatcher() {
            @Override
            public Object loadObject() throws Exception {
                System.out.println("Dispatcher loadObject() ...");
                PropertyBean bean = new PropertyBean();
                bean.setPropertyName("Dispatcher lazy-load object propertyName!");
                bean.setPropertyValue(11);
                return bean;
            }
        });
        PropertyBean o = (PropertyBean) enhancer.create();
        return o;
    }

}
  1. 测试类
@Test
public void testDisPatcher() {

    LoaderBeanDispatcher loader = new LoaderBeanDispatcher();
    System.out.println(loader.getLoaderName());
    System.out.println(loader.getLoaderValue());
    PropertyBean propertyBean = loader.getPropertyBean();
    System.out.println("--------------------");
    System.out.println(propertyBean.getPropertyName());
    System.out.println(propertyBean.getPropertyValue());
    System.out.println("after-------------");
    System.out.println(propertyBean.getPropertyName());
    // 每次调用都会获取不同的对象
}
1.2.5.2 运行结果
loaderNameA
123
--------------------
Dispatcher loadObject() ...
Dispatcher lazy-load object propertyName!
Dispatcher loadObject() ...
11
after-------------
Dispatcher loadObject() ...
Dispatcher lazy-load object propertyName!
1.2.5.3 结果分析

Dispatcher 与 LazyLoader 的不同之处在于,每次去获取对象的时候都会创建一个新的对象,而不是复用同一个对象

1.2.6 NoOp

1.2.6.1 测试方法
package com.wanlong.cglib;

import net.sf.cglib.proxy.CallbackHelper;
import net.sf.cglib.proxy.FixedValue;
import net.sf.cglib.proxy.NoOp;

import java.lang.reflect.Method;

public class CallbackHelperImpl extends CallbackHelper {
    public CallbackHelperImpl(Class superclass, Class[] interfaces) {
        super(superclass, interfaces);
    }

    @Override
    protected Object getCallback(Method method) {
        // 对这个方法就行增强,其他的方法不改变
        if (method.getName() == "sayHello") { 
            return new FixedValue() {
                @Override
                public Object loadObject() throws Exception {
                    System.out.println("Hello cglib");
                    return new User();
                }
            };
        } else { // NoOp 就可以在这种情况下使用
            return new NoOp() {
            };
        }
    }
}
@Test
public void testNoOp() {

    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(User.class);
    //enhancer.setInterfaces(User.class.getInterfaces()); //不能使用接口,只能使用类
    enhancer.setCallback(new NoOp() {
    });
    User proxy = (User) enhancer.create();
    proxy.sayHello("老王");
}

@Test
public void testNoOp2() {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(User.class);
    CallbackHelper callbackHelper = new CallbackHelperImpl(User.class, null);
    enhancer.setCallbackFilter(callbackHelper);
    enhancer.setCallbackTypes(callbackHelper.getCallbackTypes());
    enhancer.setCallbacks(callbackHelper.getCallbacks());
    User proxy = (User) enhancer.create();
    proxy.sayHello("Ray");
    proxy.sayHello2("wewewe"); // 其他方法的调用不受影响
}
1.2.6.2 运行结果

测试1结果:

你好,老王我是目标对象

测试2结果:

Hello cglib
你好2+wewewe我是目标对象
1.2.6.3 结果分析

测试1可以看到,实际就是目标类的方法调用,op啥也没做
测试2可以看到,当方法名是sayHello的时候,代理类用的是回调函数FixedValue,当是其他方法的时候,代理类啥也不做,用的是目标类的方法
这个回调什么也不做,会完全继承被代理类的功能,所以 NoOp 不能使用接口来创建代理,只能使用类。如果这样看,这个回调函数好像没啥用。代理类不会扩展原来的功能,我可以直接用原来的类。
在第二个例子中,举例了回调NoOp的一种用法,当我们需要某个方法被拦截,其他方法保持目标类行为的时候,可以通过CallbackFilter 和 NoOp 实现

2 动态代理

关于动态代理,静态代理,我后续会单独建一个设计模式专栏,在里面详细介绍。

3 扩展

3.1 javassist

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。相对于asm等这些工具,开发者不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

特点:相对ASM来说,API更易使用,性能比ASM差。

3.2 asm

asm是assembly的缩写,是汇编的称号,对于java而言,asm就是字节码级别的编程

ASM是一个Java字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

特点:小巧,快速,编写难度高,需要对字节码文件以及JVM深入了解。

参考文档:

cglib介绍

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值