前言
我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是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 测试方法
- 待懒加载类
package com.wanlong.cglib;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class PropertyBean {
private String propertyName;
private int propertyValue;
}
- 加载类(属性包含上面待懒加载类)
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;
}
}
- 测试
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 测试方法
- 替换上面案例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;
}
}
- 测试类
@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深入了解。