3-java安全基础——jdk动态代理
Posted songly_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3-java安全基础——jdk动态代理相关的知识,希望对你有一定的参考价值。
静态代理
什么是静态代理,假设现在有这样一个需求:要求在所有类的addUser方法前后添加打印日志。那么如何在不修改源代码的情况下完成需求?
通常做法是:为每一个目标类创建一个代理类,并让目标类和代理类实现同一个接口。在创建代理对象的时候通把目标对象传入构造中,然后在代理对象内部的方法中调用目标对象的方法前后输出日志。
静态代理实现:
//同一接口
interface UserService {
public void addUser();
}
//目标类
class UserServiceImpl implements UserService{
@Override
public void addUser() {
System.out.println("addUser()......");
}
}
//代理类
class UserServiceProxy implements UserService{
UserServiceImpl userService;
public UserServiceProxy(UserServiceImpl userService){
this.userService = userService;
}
@Override
public void addUser() {
//调用前输出日志
System.out.println("addUser()......before");
userService.addUser();
//调用后输出日志
System.out.println("addUser()......after");
}
}
public class ProxyTest {
public static void main(String[] args) throws Exception{
UserServiceImpl userService = new UserServiceImpl();
UserServiceProxy userServiceProxy = new UserServiceProxy(userService);
userServiceProxy.addUser();
}
}
程序输出结果:
静态代理存在的问题:
从上图可以看到,静态代理要求每一个目标类都要创建一个代理类并实现同一接口,这意味着如果有多个目标类的话就要创建多个代理对象,那么我们如何少写代理对象呢?
复习一下对象创建过程:前面在学习反射的动态类加载时说过,当使用new操作创建一个对象时,jvm会通过ClassLoader类加载器把该类的.class文件加载到内存中并为之生成一个Class对象,并且当创建多个对象时,ClassLoader类加载器只会在第一次创建对象时加载.class文并创建Class对象(一个类只有一个Class对象),而Class对象是Class类的实例,也就是说Class类可以描述所有类。
动态代理
思考这样一个问题:能否不写代理类,直接获得代理Class对象,然后根据它创建代理实例?因为我们知道Class对象包含了一个类的完整结构信息(成员变量,成员方法,构造器等等),那么Class对象如何获取?
下面给出一位大佬的解决方案:
代理类和目标类理应实现同一组接口,之所以实现相同接口,是为了尽可能保证代理对象的内部结构和目标对象一致,这样我们对代理对象的操作最终都可以转移到目标对象身上,代理对象只需专注于增强代码的编写。所以,可以这样说:接口拥有代理对象和目标对象共同的类信息。所以,我们可以从接口那得到理应由代理类提供的信息。但是别忘了,接口是无法创建对象的,怎么办?
jdk动态代理提供了InvocationHandler接口和Proxy类来解决以上问题:
java.lang.reflect.InvocationHandler
java.lang.reflect.Proxy
Proxy类有一个getProxyClass静态方法,该函数定义如下:
public static Class<?> getProxyClass(ClassLoader loader , Class<?>... interfaces)
getProxyClass方法可以获取代理类的Class对象,该方法要求传入一个类加载器和一组接口信息,返回一个代理Class对象。
参数loader:一般为接口的类加载器
参数interfaces:接口的Class对象
我们知道getProxyClass方法在返回代理Class对象之前,肯定要先获得接口的Class对象,getProxyClass方法会从你传入的接口Class中,“拷贝”类结构信息到一个新的Class对象中,但新的Class对象带有构造器,是可以创建对象的。
如上图所示,getProxyClass方法会根据参数loader 和参数interfaces得到接口Class对象(UserService.class),再根据接口Class对象创建一包含接口方法,还有构造器的代理Class对象,这样就可以通过代理Class对象来创建代理对象实例。
根据代理Class对象的构造器创建代理对象实例时需要传入InvocationHandler,因为在调用目标对象方法时,需要用到InvocationHandler(不要问为什么,先记住继续往下看)
Constructor<?> constructors = proxyInstance.getConstructor(InvocationHandler.class);
接下来我们就可以在invocationHandler中手动创建一个目标对象
public static void main2(String[] args) throws Exception {
/*
参数1:接口的类加载器
参数2:代理对象和目标类需要实现同一接口UserService的Class对象
根据接口Class创建代理Class对象
*/
Class<?> proxyInstance = Proxy.getProxyClass(UserService.class.getClassLoader() , UserService.class);
//得到有参构造,需要传入InvocationHandler,通过InvocationHandler.invoke方法调用目标对象方法
Constructor<?> constructors = proxyInstance.getConstructor(InvocationHandler.class);
//代理Class创建代理对象实例
UserService userServiceProxy = (UserService)constructors.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//创建目标对象
UserServiceImpl userService = new UserServiceImpl();
//打印日志
System.out.println("addUser()......before");
//invoke方法内部会调用目标实现类的addUser方法
method.invoke(userService , args);
//打印日志
System.out.println("addUser()......after");
return null;
}
});
//调用目标方法
userServiceProxy.addUser();
}
代理对象调用目标方法的大概流程:
但在实际的使用中我们不会用getProxyClass方法,而是使用newProxyInstance方法:
public static Object newProxyInstance(ClassLoader loader , Class<?>[] interfaces , InvocationHandler h);
参数说明:
loader :目标实现类的类加载器(这里的目标实现类指的就是被代理类)
interfaces :目标实现类实现的所有接口
h:需要传入一个invocationHandler
为什么需要参数h?和getProxyClass方法一样,因为代理对象需要借助invocationHandler的invoke方法,然后通过invoke方法可以调用被代理对象的方法。
newProxyInstance方法会根据以上三个参数返回一个Object类型的代理对象实例,然后需要把代理对象转换成UserService类型,这里大家先思考一下:
为什么要进行强制类型转换?
好了,现在我们直接来看下面的代码:
从代码中可以看到,这里是可以不进行强转的话就无法调用被代理对象中的addUser方法,为什么是用不了addUser方法的方法?
原因在于类型不匹配,既然类型不匹配那转换成UserServiceImpl类型可以吗?可以看到程序抛出ClassCastException异常,类型依然不匹配。
再把代理对象转换成UserService接口类型,最终的动态代理代码:
可以看到代理对象成功调用addUser方法,那么问题来了,代理对象userServiceProxy所属类型为什么是UserService类型?在前面的动态代理解决方案中有提到过,代理对象和目标类必须实现同一接口UserService,既然代理对象实现了UserService接口,那么就可以通过接口类型指向代理对象(多态的体现),这样自然就可以调用代理对象的addUser方法了。
通过一个示例程序来帮助理解:
MyProxy类实现了UserService接口,userService是一个接口引用,可以看到在判断userService所属类型时无论MyProxy还是UserService都返回true,原因在于多态,但在运行阶段userService所属的对象类型实际上是MyProxy类的class对象类型。
再回到代理类$Proxy0,为了验证我们的推测,需要查看代理类$Proxy0的源代码。
一般来说代理类$Proxy0是由jvm底层自动生成,通常是看不到的,但要查看代理类$Proxy0的源码还是有办法的,不过需要借助一个反编译工具Luyten进行反编译代理类$Proxy0生成的class文件,得到的源代码如下
import com.test.*;
import java.lang.reflect.*;
public final class $Proxy0 extends Proxy implements UserService
{
//实现或重写的方法
private static Method m1;
private static Method m2;
private static Method m0;
private static Method m3;
//构造需要传入一个invocationHandler
public $Proxy0(final InvocationHandler invocationHandler) {
//调用父类的构造
super(invocationHandler);
}
public final boolean equals(final Object o) {
try {
return (boolean)super.h.invoke(this, $Proxy0.m1, new Object[] { o });
}
catch (Error | RuntimeException error) {
throw;
}
catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
}
public final String toString() {
try {
return (String)super.h.invoke(this, $Proxy0.m2, null);
}
catch (Error | RuntimeException error) {
throw;
}
catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
}
public final int hashCode() {
try {
return (int)super.h.invoke(this, $Proxy0.m0, null);
}
catch (Error | RuntimeException error) {
throw;
}
catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
}
//重写接口的方法
public final void addUser() {
try {
//调用invoke方法
super.h.invoke(this, $Proxy0.m3, null);
}
catch (Error | RuntimeException error) {
throw;
}
catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
}
//静态代码块
static {
try {
$Proxy0.m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
$Proxy0.m2 = Class.forName("java.lang.Object").getMethod("toString", (Class<?>[])new Class[0]);
$Proxy0.m0 = Class.forName("java.lang.Object").getMethod("hashCode", (Class<?>[])new Class[0]);
//通过反射获取接口的方法
$Proxy0.m3 = Class.forName("com.test.UserService").getMethod("addUser", (Class<?>[])new Class[0]);
}
catch (NoSuchMethodException ex) {
throw new NoSuchMethodError(ex.getMessage());
}
catch (ClassNotFoundException ex2) {
throw new NoClassDefFoundError(ex2.getMessage());
}
}
}
从源码中可以看到,jvm底层生成的代理类$Proxy0继承了Proxy类,实现了UserService接口并重写了接口的所有方法,并且代理类$Proxy0的构造要求传入一个invocationHandler,实际上是调用了Proxy类中的invocationHandler。
代理类$Proxy0把重写的父类方法或实现的接口方法封装成了成员属性Method,并且每一个方法内部都调用了invocationHandler的invoke方法(应该明白一点:代理类$Proxy0会为UserService中的每一个方法都调用invoke方法)。这里我们只分析addUser方法,发现该方法内部也调用了invocationHandler的invoke方法,传了两个参数,一个是代理类$Proxy0当前对象,另一个是成员属性Method指向的方法(Method里的方法是通过反射获取的)。InvocationHandler是一个接口,因此会调用该接口实现类的invoke方法,这样invocationHandler的invoke方法就可以通过method.invoke方法(通过反射调用目标对象方法)来调用目标对象的addUser方法了。
动态代理流程梳理:
总结:
动态代理和静态代理的区别在于:静态代理中的代理对象是手动创建的,而动态代理中的代理对象是底层自动生成的。
参考资料:
以上是关于3-java安全基础——jdk动态代理的主要内容,如果未能解决你的问题,请参考以下文章