利用Jdk动态代理模拟MyBatis的Mapper功能

本文将先介绍jdk动态代理的基本用法,并对其原理和注意事项予以说明。之后将以两个最常见的应用场景为例,进行代码实操。这两个应用场景分别是拦截器声明性接口,它们在许多开发框架中广泛使用。比如在spring和mybatis中均使用了拦截器模式,在mybatis中还利用动态代理来实现声明性接口的功能。因此,掌握动态代理的原理和代码书写方式,对阅读理解这些开源框架非常有益。

文中的示例代码基于jdk8编写,且都经过验证,但在将代码迁移到博客的过程中,难免存在遗漏。如果您将代码复制到自己的IDE后无法运行,或存在语法错误,请在评论中留言指正 😉

小示例

先来看一个jdk代理的最小demo

点击查看代码
package demo.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JdkProxyBasicDemo {

    // ⑴ 定义业务接口
    interface BusinessInterface {
        void greeting(String str);
    }

    // ⑵ 编写代理逻辑处理类
    static class ProxyLogicHandler implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.printf("运行的代理类为: %s\n", proxy.getClass().getName());
            System.out.printf("调用的代理方法为: %s\n", method.getName);
            System.out.printf("调用方法的参数为: %s\n", args[0]);
            System.out.println("请在这里插入代码逻辑代码...");  // ⑵.1
            return null;                                   // ⑵.2
        }
    }

    // ⑶ 生成代理实例,并使用
    public static void main(String[] args) {
        ProxyLogicHandler proxyLogicHandler = new ProxyLogicHandler();
        Class[] interfaces = new Class[]{BusinessInterface.class},
        BusinessInterface businessProxy = (BusinessInterface) Proxy.newProxyInstance(BusinessInterface.class.getClassLoader(), proxyLogicHandler);
        businessProxy.greeting("Hello, Jdk Proxy");
    }
}

上述代码执行后的输出结果如下:

运行的代理类为: class com.sun.proxy.$Proxy0
调用的代理方法为: greeting
调用方法的参数为: Hello, Jdk Proxy
请在这里插入代理的逻辑代码...

其中倒数第二行的businessProxy变量,就是一个代理对象,它是BusinessInterface接口的一个实例,但我们并没有编写这个接口的实现类,而是通过Proxy.newProxyInstance方法生成出了该接口的实例。那么这个动态代理实例对应的Class长什么样子呢?上面的结果输出中已经打印出来了,这个代理类名称为com.sun.proxy.$Proxy0。实际上,如果我们再为另外一个接口生成代理对象的话,它的Class名称为com.sun.proxy.$Proxy1,依次类推。

还有一个值得关注的问题:最重要的逻辑代码应该写在哪里?答案是写在InvocationHandler这个接口的invoke()方法中,也就是上面示例代码的第⑵处。由此可以看出:代理对象实际要执行的代码,就是invoke()方法中的代码,换言之,代理对象所代理的所有接口方法,最终要执行的代码都在invoke方法里,因此,这里是一切魔法的入口。

编写一个jdk代理实例的基本步骤如下:

  1. 编写业务接口
    因为jdk代理是基于接口的,因此,只能将业务方法定义成接口,但它可以一次生成多个接口的代理对象

  2. 编写调用处理器
    即编写一个java.lang.reflect.InvocationHandler接口的实现类,代理对象的业务逻辑就写在该接口的invoke方法中

  3. 生成代理对象
    有了业务接口和调用处理器后,将二者作为参数,通过Proxy.newProxyInstance方法便可以生成这个(或这些)接口的代理对象。比如上述示例代码中的businessProxy对象,它拥有greeting()这个方法,调用该方法时,实际执行的就是invoke方法。

代理对象生成原理

代理的目的,是为接口动态生成一个实例对象,该对象有接口定义的所有方法。调用对象的这些方法时,都将执行生成该对象时,指定的“调用处理器”中的方法(即invoke方法)。

生成代理对象的方法签名如下:

Proxy.newProxyInstance (ClassLoader loader,  Class<?>[] interfaces, InvocationHandler handler)

classloader一般选择当前类的类加载器,interfaces是一个接口数组,newProxyInstance方法将为这组接口生成实例对象,handler中的代码则是生成的实例对象实际要执行的内容,这些代码就位于invoke方法中。在生成代理对象前,会先生成一个Class,这个Class实现了interfaces中的所有接口,且这些方法的内容为直接调用handler#invoke,如下图所示:

JDK代理对象生成原理

特别说明
InvocationHandler的invoke方法签名为:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable

在该方法的实现代码中,不要调用proxy参数的toString方法, 这会导致递归死循环

下面将以小示例中的BusinessInterface接口和ProxyLogicHandler为基础,用普通Java代码的方式,模拟一下Proxy.newProxyInstance的代码逻辑,如下:

点击查看代码
public static Object newProxyInstance(ClassLoader loader,  Class<?>[] interfaces, InvocationHandler handler) {
    return new Proxy0(handler);
}

static class Proxy0 implements BusinessInterface{
    private InvocationHandler handler;

    BusinessInterface(InvocationHandler handler) {
        this.handler = handler;
    }

    @Override
    public void greeting(String str) {
        handler.invoke(this, 'greeting', new Object[]{str});
    }
}

上面的代码是示意性的,并不正确,比如它没有使用到loader和interfaces参数,调用hanlder.invoke方法时,对于method参数只是简单的用'greeting'字符串替代,类型都不正确。但这段示意代码很简单明了地呈现了真实的Proxy.newProxyInstance方法内部的宏观流程。

下面再提供一个与真实的newProxyInstance方法稍微接近一点的模拟实现(需要您对jdk里JavaCompiler类的使用有一定了解)

点击查看代码
package guzb.diy.proxy;

import javax.tools.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class ImitateJdkProxy {

    public static void main(String[] args) throws Throwable{
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("执行invocationHandler#invoke()方法");
                System.out.println("调用的代理方法名为:" + method.getName());
                System.out.println("调用时传递的参数为:" + args[0]);
                return null;
            }
        };
        Foo foo = (Foo) newProxyInstance(ImitateJdkProxy.class.getClassLoader(), Foo.class, handler);
        foo.sayHi("East Knight");
    }

    /** 
     * 模拟java.lang.reflect.Proxy#newProxyInstance方法
     * 这里简化了代理类的类名,固定为:guzb.diy.$Proxy0
     */
    public static final Object newProxyInstance(ClassLoader loader, Class<?> interfaces, InvocationHandler handler) throws Exception {
        // 1. 构建代理类源码对象
        JavaFileObject sourceCode = generateProxySourceCode();

        // 2. 编译代理源代码
        JavaBytesFileObject byteCodeFile = new JavaBytesFileObject("guzb.diy.$Proxy0");
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager stdFileManager = compiler.getStandardFileManager(null, Locale.CHINA, Charset.forName("utf8"));
        JavaFileManager fileManager = new ForwardingJavaFileManager(stdFileManager) {
            @Override
            public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
                return byteCodeFile;
            }
        };
        List<JavaFileObject> compilationUnits = new ArrayList<>();
        compilationUnits.add(sourceCode);
        JavaCompiler.CompilationTask compilationTask = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
        if (!compilationTask.call()) {
            return null;
        }

        // 3. 加载编译后的代理类字节码
        loader = new ClassLoader() {
            @Override
            public Class<?> findClass(String name) throws ClassNotFoundException {
                byte[] bytes = byteCodeFile.getBytes();
                return defineClass(name, bytes, 0, bytes.length);
            }
        };
        Class clazz = loader.loadClass("guzb.diy.$Proxy0");

        // 4. 创建代理类实例并返回
        Constructor constructor = clazz.getConstructor(new Class[]{InvocationHandler.class});
        return constructor.newInstance(handler);
    }

    /**
     * 生成代理Class的源代码,该代码将在运行期间动态编译和加载。
     * 为了便于直观查看代理类的原理,故意采用了这个使用源码编译的方式,实际上,
     * JDK真实的newProxyInstance方法,内部是采用纯反射+直接生成字节码数组的方式实现的,比较晦涩。
     * 这里也简化了代理代码,比如:
     *   1. 写死了代理类的类名:guzb.diy.$Proxy0
     *   2. 写死了要实现的接口和方法
     *      不写死的话,需要通过反射遍历所有接口的所有方法,并基于Method对象的方法名、返回类型、参数列表和异常列表,
     *      创建实现类的方法签名文本,这样的话,代码就太冗长了,干扰了对代理主线逻辑的理解,也不是本文的重点
     *   3. 没有使用调用者传递的ClassLoader来加载编译后的字节码文件,原因同上,涉及加载器的隔离问题,代码过于冗长
     */
    private static JavaFileObject generateProxySourceCode() throws NoSuchMethodException {
        String[] codeLines = new String[]{
                "package guzb.diy;",

                "import java.lang.reflect.*;",
                "import guzb.diy.proxy.ImitateJdkProxy.Foo;",

                "public class $Proxy0 implements Foo {                    ",
                "    private InvocationHandler handler;                   ",
                "                                                         ",
                "    public $Proxy0 (InvocationHandler handler) {         ",
                "        this.handler = handler;                          ",
                "    }                                                    ",
                "                                                         ",
                "    @Override                                            ",
                "    public void sayHi(String name) throws Throwable {    ",
                "        Method method = Foo.class.getMethod(\"sayHi\", new Class[]{String.class}); ",
                "        this.handler.invoke(this, method, new Object[]{name}); ",
                "    }",
                "}"
        };

        String code = "";
        for (String codeLine : codeLines) {
            code += codeLine + "\n";
        }
        return new JavaStringFileObject("guzb.diy.$Proxy0", code);
    }

    /** 一个简单的业务接口 */
    public interface Foo {
        void sayHi(String name) throws Throwable;
    }

    /** 基于字符串的Java源代码对象 */
    public static class JavaStringFileObject extends SimpleJavaFileObject {
        // 源代码文本
        final String code;

        /**
         *  @param name Java源代码文件名,要包含完整的包名,比如guzb.diy.Proxy
         *  @param code Java源代码文本
         */
        JavaStringFileObject(String name, String code) {
            super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension), Kind.SOURCE);
            this.code = code;
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return code;
        }
    }

    /** 编译后的字节码文件 */
    public static class JavaBytesFileObject extends SimpleJavaFileObject {
        // 接收编译后的字节码
        private ByteArrayOutputStream byteCodesReceiver;

        /** @param name Java源代码文件名,要包含完整的包名,比如guzb.diy.Proxy */
        protected JavaBytesFileObject(String name) {
            super(URI.create("bytes:///" + name + name.replace(".", "/")), Kind.CLASS);
            byteCodesReceiver = new ByteArrayOutputStream();
        }

        @Override
        public OutputStream openOutputStream() throws IOException {
            return byteCodesReceiver;
        }

        public byte[] getBytes() {
            return byteCodesReceiver.toByteArray();
        }
    }
}

代码运行结果为:

执行invocationHandler#invoke()方法
调用的代理方法名为:sayHi
调用时传递的参数为:East Knight

应用场景

上面提到:代理是在运行期,为接口动态生成了一个实现类,和这个实现类的实例。那这个功能有什么用呢?我们直接写一个实现类不也是一样的么?代理类与我们手动写代码的主要差异在于它的动态性,它允许我们在程序的运行期间动态创建Class,这对于框架类程序,为其预设的业务组件增加公共特性提供了技术支持。因为这种额外特性的加持,对业务代码没有直接的侵入性,因此效果非常好。动态代理的两个最常用见应用场景为拦截器和声明性接口,下面分别介绍。

拦截器功能

搭载器就是将目标组件劫持,在执行目标组件代码的前后,塞入一些其它代码。比如在正式执行业务方法前,先进行权限校验,如果校验不通过,则拒绝继续执行。对于此类操作,业界已经抽象出一组通用的编程模型:面向切面编程AOP

接下来,将以演员和导演为业务背景,实现一个简易的拦截器,各个组件介绍如下:

  • Performer <Interface>
    演员接口,有play和introduction方法

  • DefaultActor <Class>
    代表男性演员,它实现了Performer接口,也是拦截器将要拦截的对象

  • Director <Interface>
    导演接口,只有一个getCreations方法, 该方法返回一个字符串列表,它代表导演的作品集

  • DefaultDirector <Class>
    Director接口的实现类,同时也是拦截器将要拦截的对象

  • ProxyForInterceptor <Class>
    拦截器核心类,实现了InvocationHandler接口,拦截器代码位于接口的invoke方法中。

    拦截器将持有Performer和Direcotor的真实实现实例,并在调用Performer的play和introduction方法前,先执行一段代码。这里实现为打印一段文本,接着再调用play或introduction,执行完后,再执行一段代码,也是打印一段文本。Director实例方法的拦截处理逻辑与此相同。这便是最简单的拦截器效果了。

  • IntercepterTestMain <Class>
    拦截器测试类,在main方法中,验证上述组件的拦截器功能效果。这个例子中,特意写了两个接口和两个实现类,就是为了演示,JDK的动态代理是支持多接口的。

下面是各个组件的源代码

Performer
package guzb.diy.proxy;

/**
 * 演员接口
 * 在这个示例中,将为该接口生成代理实例
 */
public interface Performer {
    /**
     * 根据主题即兴表演一段
     * @param subject 表演的主题
     */
    void play(String subject);

    /** 自我介绍 */
    String introduction();
}
DefaultActor
package guzb.diy.proxy;

/**
 * 这是演员接口的默认实现类
 * 在本示例中,它将作为原始的接口实现者,被代理(拦截)
 */
public class DefaultActor implements Performer {
    @Override
    public void play(String subject) {
        System.out.println("[DefaultActor]: 默认男演员正在即兴表演《"+ subject +"》");
    }

    @Override
    public String introduction() {
        return "李白·上李邕: 大鹏一日同风起,扶摇直上九万里。假令风歇时下来,犹能颠却沧溟水。世人见我恒殊调,闻余大言皆冷笑。宣父尚能畏后生,丈夫未可轻年少。";
    }
}
Director
package guzb.diy.proxy;

import java.util.List;

/**
 * 导演接口
 * 在这个示例中,将为该接口生成代理实例
 */
public interface Director {
    /**
     * 获取曾导演过的作品集
     * @return 作品名称列表
     */
    List<String> getCreations();
}

DefaultDirector
package guzb.study.javacore.proxy.jdk;

import java.util.ArrayList;
import java.util.List;

/**
 * 这是导演接口的默认实现类
 * 在本示例中,它将作为原始的接口实现者,被代理(拦截)
 */
public class DefaultDirector implements Director{
    @Override
    public List<String> getCreations() {
        return new ArrayList(){
            {
                add("活着");
                add("盲井");
                add("走出夹边沟");
                add("少年派的奇幻漂流");
            }
        };
    }
}
ProxyForInterceptor
package guzb.diy.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 代理应用场景一:拦截器
 * 即在原来的业务逻辑上追加额外的代码,这是代理功能最常见的应用场景。
 *
 * 在本示例中,导演与演员实例代表原始业务,
 * 由于代理的目的是在执行真实的接口实现类方法的前后,执行一段其它代码。
 * 因此,本类需要持有原始的导演和演员实例。
 */
public class ProxyForInterceptor implements InvocationHandler {

    // 原始的演员对象
    private Performer performer;

    // 原始的导演对象
    private Director director;

    public ProxyForInterceptor(Director director, Performer performer) {
        this.director = director;
        this.performer = performer;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        System.out.printf("[DirectorActorProxyHandler]: 调用的代理方法为:%s\n", methodName);

        System.out.printf("[DirectorActorProxyHandler]: >>> 调用 %s 之前的逻辑\n", methodName);

        Object result = null;
        // 因为本代理处理器,只针对Director和Actor接口,因此,如果方法名为play,则一定调用的是Actor的play方法
        // 根据Actor#play方法的参数定义,它只有一个String参数,所以直接取args[0]即可
        if(methodName.equals("play")) {
            performer.play((String)args[0]);
        } else if (methodName.equals("introduction")) {
            result = performer.introduction();
        } else if (methodName.equals("getCreations")) {
            result = director.getCreations();
        }

        System.out.printf("[DirectorActorProxyHandler]: <<< 调用 %s 之后的逻辑\n", methodName);
        return result;
    }
}
IntercepterTestMain
package guzb.diy.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.List;

public class IntercepterTestMain {
    public static void main(String[] args) {
        Performer actor = new DefaultActor();
        Director director = new DefaultDirector();
        InvocationHandler interceptor = new ProxyForInterceptor(director, actor);

        // 要代理的接口,这里称之为委托接口,即委托给代理实例,去实现相应的功能
        Class[] principalInterfaces = new Class[]{Director.class, Performer.class};

        // 创建一个代理实例,该实例实现了委托接口所定义的方法,因此,这个实例可以强转为Performer和Director
        Object directorPerformerProxy = Proxy.newProxyInstance(IntercepterTestMain .class.getClassLoader(), principalInterfaces, interceptor);

        Performer performerProxy = (Performer) directorPerformerProxy;
        Director directorProxy = (Director) directorPerformerProxy;

        // ① 调用代理实例中,Performer接口相关的方法
        performerProxy.play("长板坡");
        String introduction = performerProxy.introduction();
        System.out.printf("[IntercepterTestMain ]: 代理对象返回的个人简介内容为: %s\n", introduction);

        // 调用代理实例中,Director接口相关的方法
        List<String> creations = directorProxy.getCreations();
        System.out.println("[IntercepterTestMain ]: 代理对象返回的导演作品列表:");
        for (String creation : creations) {
            System.out.printf("    · %s\n", creation);
        }
    }
}

以上代码的执行结果如下:

[DirectorActorProxyHandler]: 调用的代理方法为:play
[DirectorActorProxyHandler]: >>> 调用 play 之前的逻辑
[DefaultActor]: 默认男演员正在即兴表演《长板坡》
[DirectorActorProxyHandler]: <<< 调用 play 之后的逻辑
[DirectorActorProxyHandler]: 调用的代理方法为:introduction
[DirectorActorProxyHandler]: >>> 调用 introduction 之前的逻辑
[DirectorActorProxyHandler]: <<< 调用 introduction 之后的逻辑
[IntercepterTestMain ]: 代理对象返回的个人简介内容为: 李白·上李邕: 大鹏一日同风起,扶摇直上九万里。假令风歇时下来,犹能颠却沧溟水。世人见我恒殊调,闻余大言皆冷笑。宣父尚能畏后生,丈夫未可轻年少。
[DirectorActorProxyHandler]: 调用的代理方法为:getCreations
[DirectorActorProxyHandler]: >>> 调用 getCreations 之前的逻辑
[DirectorActorProxyHandler]: <<< 调用 getCreations 之后的逻辑
[IntercepterTestMain ]: 代理对象返回的导演作品列表:
    · 活着
    · 盲井
    · 走出夹边沟
    · 少年派的奇幻漂流

可以看到,在main方法中,调用代理类的play方法后(位于代码的①处),在执行真实的DefaultActor#play方法前后,均有额外的文本输出,这些都不是DefaultActor#play方法的逻辑。这便实现了拦截器效果,且对于使用者而言(即编写DefaultActor类的开发者),是无侵入无感知的。

声明性接口

声明性接口的特点是:开发者只需要提供接口,并在接口方法中声明该方法要完成的功能(通常是以多个注解的方式声明),但不用编写具体的功能实现代码,而是通过框架的工厂方法来获取该接口的实例。当然,该实例会完成接口方法中所声明的那些功能。比较典型的产品是MyBatis的Mapper接口。实现手段也是采用jdk动态代理,在InvocationHandler的invoke方法中,完成该接口方法所声明的那些特性功能。

接下来,本文将模拟MyBatis的Mapper功能,组件说明如下:

  • SqlMapper <Annotaton>
    与MyBatis的Mapper注解等效,用于标识一个接口为Sql映射接口,但在本示例中,这个接口并未使用到。因为这个标识接口的真实用途,是在SpringBoot环境中,用于自动扫描和加载Mapper接口的。本示例仅模拟Mapper本身的声明性功能,因此用不上它。保留这个接口,只是为了显得更完整。

  • Select <Annotation>
    与MyBatis的Select注解等效,它有一个sql属性,用于指定要执行的SQL语句,且支持#{}形式的插值

  • ParamName <Annotation>
    与MyBatis的Param注解等效,用于标识Mapper接口的方法参数名称,以便用于Select注解中sql语句的插值替换

  • PerformerMapper <Interface>
    演员实体的数据库访问接口,与开发者使用MyBatis时,日常编写的各类Mapper接口一样。在里边定义各种数据库查询接口方法,并利用Select和ParamName注解,声明数据操作的具体功能。

  • ProxyForDeclaration <Class>
    整个Mapper功能的核心类,实现了InvocationHandler接口,在invoke方法中,完成Mapper的所有功能

  • DeclarationTestMain <Class>
    声明性接口的功能测试类,在main方法中,通过jdk代理获得一个PerformerMapper实例,并调用其中的getQuantityByNameAndAage、getRandomPoetryOf和listAllOfAge方法,分别传入不的SQL和参数,用以验证3种不同的情况。

下面是各个组件的源代码:

SqlMapper
package guzb.diy.proxy;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 标识一个接口是一个SQL映射类,用于模拟MyBatis的mapper功能
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SqlMapper {
}
Select
package guzb.diy.proxy;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 为一个mapper方法指定查询类sql语句
 * 本类用于模拟MyBatis的mapper功能
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Select {
    /**
     * 查询sql语句,支持#{}这样的插值占位符
     */
    String sql();
}

ParamName
package guzb.diy.proxy;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 为一个mapper方法的参数,指定一个名称,以便在sql语句中进行插值替换
 * 本类用于模拟MyBatis的mapper功能
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamName {
    /** 参数的名称 */
    String value();
}
PerformerMapper
package guzb.diy.proxy;

/**
 * 演员实体查询接口。
 * 本类用于模拟MyBatis的mapper功能
 */
@SqlMapper
public interface PerformerMapper {

    @Select(sql = "select count(*) from performer where name=#{name} and age = #{ age }")
    Long getQuantityByNameAndAage(@ParamName("name") String name, @ParamName("age") Integer age);

    @Select(sql = "select poetry_item from poetry where performer_name = #{ name }")
    String getRandomPoetryOf(@ParamName("name") String name);

    // ② SQL中故障引入了一个pageSize的变量,由于方法签名中没有声明这个参数,因此会导致SQL在插值替换阶段发生异常
    @Select(sql = "select * from performer where age >= #{age} limit #{ pageSize }")
    Object listAllOfAge(@ParamName("age") int age);

}
ProxyForDeclaration
package guzb.diy.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 〔声明性接口〕功能的核心实现类
 */
public class ProxyForDeclaration implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.printf("[ProxyForDeclaration]: 调用的方法名为:%s\n", method.getName());

        // 1. 先提取出原始的SQL
        String rawSql = extractSql(method);
        if (rawSql == null || rawSql.trim().length() == 0) {
            System.out.printf("[ProxyForDeclaration]: 方法%s()未指定SQL语句,无法执行。请通过@Select注解指定Sql\n", method.getName());
            return null;
        }
        System.out.printf("[ProxyForDeclaration]: 原始sql为:%s\n", rawSql);

        // 2. 对原始SQL做插值替换,String类型的参数追加''号,其它类型原样替换
        String finalSql = interpolateSql(rawSql, method, args);
        System.out.printf("[ProxyForDeclaration]: 插值替换后的sql为:%s\n", finalSql);

        // 3. 模拟执行SQL语句
        return imitateJdbcExecution(finalSql, method.getReturnType());
    }

    private String extractSql(Method method) {
        Select selectAnnotation = method.getAnnotation(Select.class);
        return selectAnnotation == null ? null : selectAnnotation.sql();
    }

    private String interpolateSql(String rawSql, Method method, Object[] args) {
        // 使用正则表达式来完成插值表达式#{}的内容替换
        Pattern interpolationTokenPattern = Pattern.compile("(#\\{\\s*([a-zA-Z0-9]+)\\s*\\})");
        Matcher matcher = interpolationTokenPattern.matcher(rawSql);

        // 提取出方法参数名称与参数对象的对应关系,key为参数名(通过@ParamName注解指定),value为参数对象
        Map<String, Object> paramMap = extractParameterMap(method, args);

        // 插值替换
        String finalSql = rawSql;
        while (matcher.find()) {
            String interpolationToken = matcher.group(1);
            String parameterName = matcher.group(2);
            if (!paramMap.containsKey(parameterName)) {
                throw new SqlMapperExecuteException("未知参数:" + parameterName);
            }

            Object value = paramMap.get(parameterName);
            String valueStr = value instanceof String ? "'" + value.toString() + "'" : value.toString();
            finalSql = finalSql.replace(interpolationToken, valueStr);
        }
        return finalSql;
    }

    private Map<String, Object> extractParameterMap(Method method, Object[] args) {
        Parameter[] parameters = method.getParameters();
        if (parameters.length == 0) {
            return Collections.EMPTY_MAP;
        }

        Map<String, Object> sqlParamMap = new HashMap<>();
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            ParamName paramName = parameter.getAnnotation(ParamName.class);
            // 这里不用检查数组越界问题,因为args参数本身就是调用接口方法时的传递的参数,只要是正常调用(不是通过反射)就不会越界
            sqlParamMap.put(paramName.value(), args[i]);
        }
        return sqlParamMap;
    }

    /** 模拟执行jdbc sql, 这里仅对数字和字符串进行了模拟,其它返回null */
    private Object imitateJdbcExecution(String finalSql, Class<?> returnType) {
        if(Number.class.isAssignableFrom(returnType)){
            return (long)(Math.random() * 1000 + 1);
        }

        if (returnType == String.class) {
            String[] poetry = new String[]{
                    "黄四娘家花满蹊,千朵万朵压枝低。",
                    "留连戏蝶时时舞,自在妖莺恰恰啼。",
                    "荷尽已无擎雨盖,菊残犹有傲霜枝。",
                    "一年好景君须记,最是橙黄橘绿时。"
            };
            int index = (int)(Math.random() * 4);
            return poetry[index];
        }

        return null;
    }

    static class SqlMapperExecuteException extends RuntimeException {
        public SqlMapperExecuteException(String message) {
            super(message);
        }
    }
}

DeclarationTestMain
package guzb.diy.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.List;

/**
 * 〔声明性接口〕功能测试入口类
 */
public class DeclarationTestMain {

    public static void main(String[] args) {
        Class[] principalInterfaces = new Class[]{PerformerMapper.class};
        ProxyForDeclaration declarationHandler = new ProxyForDeclaration();
        PerformerMapper performerMapper = (PerformerMapper) Proxy.newProxyInstance(JdkProxyStudyMain.class.getClassLoader(), principalInterfaces, declarationHandler);

        Long count = performerMapper.getQuantityByNameAndAage("Jane Lotus", 47);
        System.out.printf("[DeclarationTestMain]: 代理实例方法方法的返回值为:%s\n\n", count);

        String poetryItem = performerMapper.getRandomPoetryOf("杜甫");
        System.out.printf("[DeclarationTestMain]: 代理实例方法的返回值为:%s\n\n", poetryItem);

        // ③ 本方法调用后将发生异常,因为PerformerMapper中的②处,声明的SQL有未知的插值变量,这里特意测试验证
        performerMapper.listAllOfAge(100);
    }

}

以上代码的执行结果为:

[ProxyForDeclaration]: 调用的方法名为:getQuantityByNameAndAage
[ProxyForDeclaration]: 原始sql为:select count(*) from performer where name=#{name} and age = #{ age }
[ProxyForDeclaration]: 插值替换后的sql为:select count(*) from performer where name='Jane Lotus' and age = 47
[DeclarationTestMain]: 代理实例方法方法的返回值为:40

[ProxyForDeclaration]: 调用的方法名为:getRandomPoetryOf
[ProxyForDeclaration]: 原始sql为:select poetry_item from poetry where performer_name = #{ name }
[ProxyForDeclaration]: 插值替换后的sql为:select poetry_item from poetry where performer_name = '杜甫'
[DeclarationTestMain]: 代理实例方法的返回值为:黄四娘家花满蹊,千朵万朵压枝低。

[ProxyForDeclaration]: 调用的方法名为:listAllOfAge
[ProxyForDeclaration]: 原始sql为:select * from performer where age >= #{age} limit #{ pageSize }
Exception in thread "main" guzb.diy.proxy.ProxyForDeclaration$SqlMapperExecuteException: 未知参数:pageSize
	at guzb.diy.proxy.ProxyForDeclaration.interpolateSql(ProxyForDeclaration.java:55)
	at guzb.diy.proxy.ProxyForDeclaration.invoke(ProxyForDeclaration.java:29)
	at com.sun.proxy.$Proxy1.listAllOfAge(Unknown Source)
	at guzb.diy.proxy.DeclarationTestMain.main(JdkProxyStudyMain.java:24)

以上代码共模拟了3个调用Mapper的场景:

  1. 调用getQuantityByNameAndAage()方法根据姓名的年龄查询演员数量。但并未真正执行JDBC查询,只是将SQL进行了插值替换和输出,然后随机返回了一个数字。这足以演示声明性接口这一特性了,真实地执行jdbc查询,那将一个代码量巨大的工作,它的缺失并不影响本示例的主旨。

  2. 调用getRandomPoetryOf()方法查询指定诗人的一段诗句。同样没有真正执行jdbc查询,而是随机返回了一句诗文。

  3. 调用listAllOfAge()方法查询指定年龄的所有演员。该方法有意设计为引发一个异常,因为接口方法上声明的SQL中,pageSize这个插值变量并未在方面签名中声明。

posted @ 2023-04-10 15:20  顾志兵  阅读(469)  评论(7编辑  收藏  举报