3

Java代理之jdk动态代理+应用场景实战 - 天地壹沙鸥

 1 year ago
source link: https://www.cnblogs.com/sandgull/p/java-proxy-via-jdk.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Java代理之jdk动态代理+应用场景实战

本文将先介绍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(original); 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代理对象生成原理

下面将以小示例中的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 <Classs>
    整个Mapper功能的核心类,实现了InvocationHandler接口,在invoke方法中,完成Mapper的所有功能

  • DeclarationTestMain <Classs>
    声明性接口的功能测试类,在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功能 */@SqlMapperpublic 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这个插值变量并未在方面签名中声明。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK