利用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代理实例的基本步骤如下:
-
编写业务接口
因为jdk代理是基于接口的,因此,只能将业务方法定义成接口,但它可以一次生成多个接口的代理对象 -
编写调用处理器
即编写一个java.lang.reflect.InvocationHandler接口的实现类,代理对象的业务逻辑就写在该接口的invoke方法中 -
生成代理对象
有了业务接口和调用处理器后,将二者作为参数,通过Proxy.newProxyInstance方法便可以生成这个(或这些)接口的代理对象。比如上述示例代码中的businessProxy对象,它拥有greeting()这个方法,调用该方法时,实际执行的就是invoke方法。
代理对象生成原理
代理的目的,是为接口动态生成一个实例对象,该对象有接口定义的所有方法。调用对象的这些方法时,都将执行生成该对象时,指定的“调用处理器”中的方法(即invoke方法)。
生成代理对象的方法签名如下:
Proxy.newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler)
classloader一般选择当前类的类加载器,interfaces是一个接口数组,newProxyInstance方法将为这组接口生成实例对象,handler中的代码则是生成的实例对象实际要执行的内容,这些代码就位于invoke方法中。在生成代理对象前,会先生成一个Class,这个Class实现了interfaces中的所有接口,且这些方法的内容为直接调用handler#invoke,如下图所示:
特别说明
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的场景:
-
调用getQuantityByNameAndAage()方法根据姓名的年龄查询演员数量。但并未真正执行JDBC查询,只是将SQL进行了插值替换和输出,然后随机返回了一个数字。这足以演示声明性接口这一特性了,真实地执行jdbc查询,那将一个代码量巨大的工作,它的缺失并不影响本示例的主旨。
-
调用getRandomPoetryOf()方法查询指定诗人的一段诗句。同样没有真正执行jdbc查询,而是随机返回了一句诗文。
-
调用listAllOfAge()方法查询指定年龄的所有演员。该方法有意设计为引发一个异常,因为接口方法上声明的SQL中,pageSize这个插值变量并未在方面签名中声明。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?