JFinal的Proxy实现原理

了解Java的动态代理后,动态创建新的代理类通过反射执行。 那JFinal的代理实现有什么不一样的地方呢,据作者介绍是为了

 * 追求性能极致:
 * 1:禁止使用 JDK 的 Method.invoke(...) 调用被代理方法
 * 2:method 存在有效的拦截器才生成代理方法 ProxyMethod
 * 3:目标类 target 存在 ProxyMethod 才生成代理类 ProxyClass

作者为了追求性能极致自己设计了一套方式完成了动态代理功能。依据上面的三点意图去看代码。


例如对User.class进行代理

作者利用Enjoy、Class Loader、Dynamic Compile 美妙结合在一起,及其精简的代码量实现代理功能。
其核心功能动态生成代理类是通过ProxyGenerator结合Enjoy生成Java源代码,再通过ProxyCompiler进行编译,最后ProxyClassLoader进行加载。Enjoy生成源代码部分完全可以手工编写,但由于Enjoy的好用程度太高,用模板的方式生成代码太省事了。

ProxyClass: 代理类描述信息,生成的代理类是不可见的,相关的属性由本类管理

    private Class<?> target; // 被代理类
    private String pkg; // 报名
    private String name; // 类名
    private String sourceCode; // 生成的Java源代码
    private Map<String, byte[]> byteCode; // 编译后的字节码
    private Class<?> clazz; // 字节码被 loadClass 后的 Class
    private List<ProxyMethod> proxyMethodList = new ArrayList<>(); // 被代理的方法列表

ProxyGenerator: 代理生成器,将ProxyClass中的描述信息以及被代理类分析后通过Enjoy生成Java源代码,这里通过类的注解,给有标注的类和方法生成代理。通过@Before进行注解,

public ProxyClass generate(Class<?> target) {
    //1. 用一个Kv对象管理需要在模板中渲染的数据
    //2. 给Kv对象填充数据
    //2.1 获取被代理类的注解,getMethodUpperInterceptors()
    //3 遍历被代理类中的方法,判断此方法是否需要被代理hasInterceptor()
    //3.1 生成一个Kv存储被代理的方法信息,用于模板渲染数据
    //3.2 产生一个ProxyMethod对象存储到ProxyClass的被代理方法列表中
    //4. 通过Enjoy进行渲染
    //4.1 生成的源代码设置到ProxyClass中,为后续编译好调用
}

这里会用到一个模板,里面会有个Invocation类,这个是调用信息类,

public class Invocation {
    private static final Object[] NULL_ARGS = new Object[0];

    private Object target;//代理
    private Method method;//被代理的方法
    private Object[] args;//方法参数
    private Callback callback;//回调
    private Interceptor[] inters;//拦截器组
    private Object returnValue;//方法返回值
   public void invoke() {
   }
}
package #(pkg);
import com.alienjun.aop.Invocation;
public class #(name)#(classTypeVars) extends #(targetName)#(targetTypeVars) {
#for(x : methodList)
    
    public #(x.methodTypeVars) #(x.returnType) #(x.name)(#for(y : x.paraTypes)#(y) p#(for.index)#(for.last ? "" : ", ")#end) #(x.throws){
        #if(x.singleArrayPara)
        #@newInvocationForSingleArrayPara()
        #else
        #@newInvocationForCommon()
        #end
        
        inv.invoke();
        #if (x.returnType != "void")
        
        return inv.getReturnValue();
        #end
    }
#end
}

#--
   一般参数情况
--#
#define newInvocationForCommon()
        Invocation inv = new Invocation(this, #(x.proxyMethodKey)L,
            args -> {
                #(x.frontReturn) #(name).super.#(x.name)(
                        #for(y : x.paraTypes)
                        (#(y.replace("...", "[]")))args[#(for.index)]#(for.last ? "" : ",")
                        #end
                    );
                #(x.backReturn)
            }
            #for(y : x.paraTypes), p#(for.index)#end);
#end
#--
   只有一个参数,且该参数是数组或者可变参数
--#
#define newInvocationForSingleArrayPara()
        Invocation inv = new Invocation(this, #(x.proxyMethodKey)L,
            args -> {
                #(x.frontReturn) #(name).super.#(x.name)(
                        p0
                    );
                #(x.backReturn)
            }
            , p0);
#end

会生成一个这样的类:

package com.alienjun;
import com.alienjun.aop.Invocation;
public class User$$EnhancerByJFinal extends User {
    
    public  void showName() {
        Invocation inv = new Invocation(this, 1L,
            args -> {
                 User$$EnhancerByJFinal.super.showName(
                    );
                return null;
            }
            );
        
        inv.invoke();
    }
}

ProxyCompiler: 自定义编译器,同时自定义了MyJavaFileManager,MyJavaFileObject 便于管理编译好的字节码存放位置,不需要存放到磁盘。


// 继承ForwardingJavaFileManager
// 的目的是将编译后的字节码存在内存中,不用写到磁盘
public static class MyJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
public Map<String, MyJavaFileObject> fileObjects = new HashMap<>();

        public MyJavaFileManager(JavaFileManager fileManager) {
            super(fileManager);
        }

        // 编译器编译完成后ClassWriter.writeClass()回调此方法,kind 为 CLASS
        // 这里返回一个JavaFileObject接口的对象,writeClass()会问他要OutputStream,所以会调用它的openOutputStream
        // 返回一个ByteArrayOutputStream 给它。
        /*
        public JavaFileObject writeClass(ClassSymbol var1) throws IOException, ClassWriter.PoolOverflow, ClassWriter.StringOverflow {
        JavaFileObject var2 = this.fileManager.getJavaFileForOutput(StandardLocation.CLASS_OUTPUT, var1.flatname.toString(), Kind.CLASS, var1.sourcefile);
        OutputStream var3 = var2.openOutputStream();
        this.writeClassFile(var3, var1);
        var3.close();
        * */
        @Override
        public JavaFileObject getJavaFileForOutput(Location location, String qualifiedClassName, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
            MyJavaFileObject javaFileObject = new MyJavaFileObject(qualifiedClassName, kind);
            fileObjects.put(qualifiedClassName, javaFileObject);
            return javaFileObject;
        }

        // 是否在编译时依赖另一个类的情况下用到本方法 ?
        @Override
        public JavaFileObject getJavaFileForInput(Location location, String className, JavaFileObject.Kind kind) throws IOException {
            JavaFileObject javaFileObject = fileObjects.get(className);
            if (javaFileObject == null) {
                javaFileObject = super.getJavaFileForInput(location, className, kind);
            }
            return javaFileObject;
        }
}

    // java文件对象,包含源文件内容、编译后的字节码
    public static class MyJavaFileObject extends SimpleJavaFileObject {

        private String source;
        // 这里巧妙的利用了 内存方式存储字节码,
        private ByteArrayOutputStream outPutStream;

        // 构建源文件,定义URI文件位置
        public MyJavaFileObject(String name, String source) {
            super(URI.create("String:///" + name + Kind.SOURCE.extension), Kind.SOURCE);
            this.source = source;
        }

        // 构建编译后的字节码文件位置
        public MyJavaFileObject(String name, Kind kind) {
            super(URI.create("String:///" + name + kind.extension), kind);
            source = null;
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            if (source == null) {
                throw new IllegalStateException("source field can not be null");
            }
            return source;
        }

        // 编译器编译完成后会调用此方法,将字节码写入到outputStream 中
        // MyJavaFileManager的getJavaFileForOutput返回的是MyJavaFileObject
        // 故此方法会被回调,
        // 同时MyJavaFileManager持有fileObjects对本对象的引用,本对象中的outPutStream写入数据后即在内存中

        @Override
        public OutputStream openOutputStream() throws IOException {
            outPutStream = new ByteArrayOutputStream();
            return outPutStream;
        }

        public byte[] getByteCode() {
            return outPutStream.toByteArray();
        }
    }

编译:

    public void compile(ProxyClass proxyClass) {
        // 获取jdk提供的Java编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        if (compiler == null) {
            throw new RuntimeException("Can not get javax.tools.JavaCompiler, check whether \"tools.jar\" is in the environment variable CLASSPATH \n" +
                    "Visit https://jfinal.com/doc/4-8 for details \n");
        }

        // 收集诊断信息列表
        DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();

        // 创建Java文件管理者
        try (MyJavaFileManager javaFileManager = new MyJavaFileManager(compiler.getStandardFileManager(collector, null, null))) {

            // 构建一个Java源文件
            MyJavaFileObject javaFileObject = new MyJavaFileObject(proxyClass.getName(), proxyClass.getSourceCode());

            // 进行编译
            Boolean result = compiler.getTask(null, javaFileManager, collector, getOptions(), null, Arrays.asList(javaFileObject)).call();
            // 错出提示
            outputCompileError(result, collector);

            Map<String, byte[]> ret = new HashMap<>();
            // 从Java文件管理者中的引用fileObjects中取出 编译好的字节码
            for (Entry<String, MyJavaFileObject> e : javaFileManager.fileObjects.entrySet()) {
                ret.put(e.getKey(), e.getValue().getByteCode());
            }

            // 设置到包装类中
            proxyClass.setByteCode(ret);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

ProxyClassLoader:类加载器,自定义的目的在于方便从内存中加载编译后的字节码

    // 当调用父类的loadClass()方法就会触发查找
    // 继承ClassLoader后重写此方法,去哪里找字节码数组
    // 这里从byteCodeMap中找到对应的字节码数组然后通过defineClass将字节码数组转为Class实例
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] bytes = byteCodeMap.get(name);
        if (bytes != null) {
            Class<?> ret = defineClass(name, bytes, 0, bytes.length);
            // 转换完成后,删掉完成的字节数组
            byteCodeMap.remove(name);
            return ret;
        }
        return super.findClass(name);
    }

示例:

public class User {
    private String name;
    private int age;

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

    //这个方法需要代理
    @Before(MyInterceptor.class)
    public void showName() {
        System.out.println("我的名字是:"+name);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public User() {
    }
}
public class MyInterceptor implements Interceptor {
    @Override
    public void intercept(Invocation inv) {
        System.out.println("拦截方法:"+inv.getMethodName());
        inv.invoke();
    }
}
public static void main(String[] args) {
    User s = com.alienjun.proxy.Proxy.get(User.class);
    s.setName("小明");
    s.showName();
}

输出:
拦截方法:showName
我的名字是:小明

此设计非常巧妙,这样就不需要cglib 、asm和jdk动态代理机制了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,确实编译语言发展的一大步。 虚拟机把描述类的数据从...
    胡二囧阅读 932评论 0 0
  • 一、基础理论知识 1、java虚拟机的生命周期: Java虚拟机的生命周期 一个运行中的Java虚拟机有着一个清晰...
    ipfs布道者阅读 303评论 0 1
  • 类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到...
    CHSmile阅读 1,594评论 0 12
  • 本文目的: 深入理解Java类加载机制; 理解各个类加载器特别是线程上下文加载器; Java虚拟机类加载机制 虚拟...
    czwbig阅读 712评论 0 3
  • 心要碎了,却还是要装作没事的样子。
    樱花落羽阅读 65评论 0 0