利用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这个插值变量并未在方面签名中声明。