本文将先介绍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,如下图所示:

特别说明

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

利用Jdk动态代理模拟MyBatis的Mapper功能的更多相关文章

  1. 利用JDK动态代理机制实现简单拦截器

    利用JDK动态代理机制实现简单的多层拦截器 首先JDK动态代理是基于接口实现的,所以我们先定义一个接口 public interface Executer { public Object execut ...

  2. MyBatis Mapper 接口如何通过JDK动态代理来包装SqlSession 源码分析

    我们以往使用ibatis或者mybatis 都是以这种方式调用XML当中定义的CRUD标签来执行SQL 比如这样 <?xml version="1.0" encoding=& ...

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

    本文将先介绍jdk动态代理的基本用法,并对其原理和注意事项予以说明.之后将以两个最常见的应用场景为例,进行代码实操.这两个应用场景分别是拦截器和声明性接口,它们在许多开发框架中广泛使用.比如在spri ...

  4. jdk动态代理与cglib动态代理例子

    1.JAVA的动态代理特征:特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息.过滤消息.把消息转发给委托类,以及事后处理消息等.代理类与委托类之间通常会存在关联关系,一个代理类的对象 ...

  5. SpringBoot27 JDK动态代理详解、获取指定的类类型、动态注册Bean、接口调用框架

    1 JDK动态代理详解 静态代理.JDK动态代理.Cglib动态代理的简单实现方式和区别请参见我的另外一篇博文. 1.1 JDK代理的基本步骤 >通过实现InvocationHandler接口来 ...

  6. 从Mybatis源码理解jdk动态代理默认调用invoke方法

    一.背景最近在工作之余,把开mybatis的源码看了下,决定自己手写个简单版的.实现核心的功能即可.写完之后,执行了一下,正巧在mybatis对Mapper接口的动态代理这个核心代码这边发现一个问题. ...

  7. 利用反射生成JDK动态代理

    利用反射生成JDK动态代理 在Java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过使用这个类和接口可以生成JDK动态代理类和动态代理 ...

  8. 静态代理和利用反射形成的动态代理(JDK动态代理)

    代理模式 代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问.在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用. 静态代理 1.新建 ...

  9. 对JDK动态代理的模拟实现

    对JDK动态代理的模拟 动态代理在JDK中的实现: IProducer proxyProduec = (IProducer)Proxy.newProxyInstance(producer.getCla ...

  10. jdk动态代理:由浅入深理解mybatis底层

    什么是代理 代理模式,目的就是为其他对象提供一个代理以控制对某个对象的访问,代理类为被代理者处理过滤消息,说白了就是对被代理者的方法进行增强. 看到这里,有没有感觉很熟悉?AOP,我们熟知的面向切面编 ...

随机推荐

  1. 深入浅出 Typescript

    TypeScript 是 JavaScript 的一个超集,支持 ECMAScript 6 标准(ES6 教程). TypeScript 由微软开发的自由和开源的编程语言. TypeScript 设计 ...

  2. 产品代码都给你看了,可别再说不会DDD(一):DDD入门

    这是一个讲解DDD落地的文章系列,作者是<实现领域驱动设计>的译者滕云.本文章系列以一个真实的并已成功上线的软件项目--码如云(https://www.mryqr.com)为例,系统性地讲 ...

  3. React-Chat移动端聊天实例|react18 hooks仿微信App聊天界面

    基于react18+react-vant+zustand仿微信手机端聊天室ReactChat. react18-chat 一款使用最新react18.x hooks.zustand搭配react-va ...

  4. 转 致创业者:APP已死 服务永生

    前几日,有位创业者和我讲他在带领团队做一个将爱踢球的人集中在一起的App,我告诉他你的创业方向错了.原因在于你的目的是要为爱踢球的人提供服务,而你现在却在竭尽全力的做App,你应该做的是设计你为爱踢球 ...

  5. 分拣平台API安全治理实战 | 京东物流技术团队

    导读 本文主要基于京东物流的分拣业务平台在生产环境遇到的一些安全类问题,进行定位并采取合适的解决方案进行安全治理,引出对行业内不同业务领域.不同类型系统的安全治理方案的探究,最后笔者也基于自己在金融领 ...

  6. SQL Server更改表字段顺序和表结构

    1.首先打开SqlServer,SSMS可视化工具.点击工具,再点选项. 2.在弹出的选项窗口中,点击Desinners,点击表设计和数据库设计器,将阻止保护勾去掉.点"确定" 3 ...

  7. 通过 Haproxy 实现 ss 负载均衡

    介绍 缺点:所有的SS的加密方式和密码必须一致 介绍:HAProxy是一个使用C语言编写的自由及开放原始码软件,其提供高可用性.负载均衡,以及基于TCP和HTTP的应用程序代理. 安装Haproxy ...

  8. 斜率优化DP 学习笔记

    斜率优化 DP 适用情况 适用于求解最优解(最大.最小)问题. 上凸壳与下凸壳 求解步骤 对于任意状态转义方程,设 \(A_i\),\(B_i\),使状态转移方程转化为 \(f_i = \min(f_ ...

  9. Haproxy搭建 Web 群集实现负载均衡

    Haproxy搭建 Web 群集实现负载均衡 1 Haproxy HAProxy是可提供高可用性.负载均衡以及基于TCP和HTTP应用的代理,是免费.快速并且可靠的一种解决方案.HAProxy非常适用 ...

  10. 【Azure Key Vault】在Azure Databricks上获取Azure Key Vault中所存储的机密(secret)的两种方式

    问题描述 在Azure Databricks上获取Azure Key Vault中所存储的机密(secret)的两种方式? 问题解答 方式一: 在Databricks的Notebook 中,直接编写P ...