2019独角兽企业重金招聘Python工程师标准>>>

昨天被问了个问题,问题的大意是这样的:为什么 Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)方法的3个参数是这样的定义的?笔者一阵语塞,好生郁闷。在这里补充一下,记录下对这个问题的解答。

基本样例

接口类

package com.vavi.proxy;public interface Sleepable {
void sleep();void eat();
}

实现类

package com.vavi.proxy;public class Person implements Sleepable {public void sleep() {
System.out.println("He is sleeping");
}public void eat() {
System.out.println("He is eating");}

}

InvocationHandler实现类

package com.vavi.proxy.dynaimc.jdk;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;public class PersonDynamicJDKProxyHandler implements InvocationHandler {
private final Object targetObject;public PersonDynamicJDKProxyHandler(Object targetObject) {
this.targetObject = targetObject;
}@Override
public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
if (method.getName().equals("eat")) {System.out.println("wash hands before eating");method.invoke(targetObject, args);System.out.println("ready to sleep now..");
} else {System.out.println("take off clothes");method.invoke(targetObject, args);System.out.println("sweet dream now..");}return null;
}
}

客户端类

package com.vavi.proxy.dynaimc.jdk;import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Proxy;import org.junit.Test;import com.vavi.proxy.Person;
import com.vavi.proxy.Sleepable;public class TestPersonDynamicJDKProxy {
@Test
public void testProxy() throws Exception {// System.getProperties().put(
// "sun.misc.ProxyGenerator.saveGeneratedFiles", true);
Person person = new Person();
PersonDynamicJDKProxyHandler handler = new PersonDynamicJDKProxyHandler(person);Sleepable proxy = (Sleepable) Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), handler);// 获取代理类的字节码
generateProxyClassFile();proxy.eat();
proxy.sleep();}private void generateProxyClassFile() {
byte[] classFile = sun.misc.ProxyGenerator.generateProxyClass("$MyProxy", Person.class.getInterfaces());FileOutputStream out = null;
String path = "/Users/ghj/startup/$MyProxy.class";
try {out = new FileOutputStream(path);out.write(classFile);out.flush();
} catch (Exception e) {e.printStackTrace();
} finally {try {out.close();} catch (IOException e) {e.printStackTrace();}
}
}
}

源码分析

执行上述程序后,系统打印如下结果:

wash hands before eating
He is eating
ready to sleep now..
take off clothes
He is sleeping
sweet dream now..

现在程序运行正常,成功实现了动态代理的效果。但是这个为什么能够得到这样的效果呢?我们跟着一步步跟踪代码执行,首先发现最重要的是(Sleepable) Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), handler);这段代码。

public static Object Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException
{if (h == null) {throw new NullPointerException();}/** Look up or generate the designated proxy class.*/Class<?> cl = getProxyClass0(loader, interfaces); // 标记1/** Invoke its constructor with the designated invocation handler.*/try {final Constructor<?> cons = cl.getConstructor(constructorParams);// 标记2final InvocationHandler ih = h;SecurityManager sm = System.getSecurityManager();if (sm != null && ProxyAccessHelper.needsNewInstanceCheck(cl)) {// create proxy instance with doPrivilege as the proxy class may// implement non-public interfaces that requires a special permissionreturn AccessController.doPrivileged(new PrivilegedAction<Object>() {public Object run() {return newInstance(cons, ih);//标记3}});} else {return newInstance(cons, ih);//标记3}} catch (NoSuchMethodException e) {throw new InternalError(e.toString());}
}

在上面代码中,标记1生成代理类的class对象,标记2获得带有这个参数constructorParams的构造器,标记3完成对象实例化。 需要提前说明的是,由于在Proxy类中,硬编码了private final static Class[] constructorParams = { InvocationHandler.class };这个属性值,所以这个一定程度了约束了我们必须要和InvocationHandler打交道了。并且隐含在代理类中,有一个带有constructorParams参数的构造器。这个在后文也会提及到。

下面接着看标记1内部实现(笔者删除了大量注释),中间进行了一些安全校验,接口个数校验,重复的接口名称等校验。

private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {SecurityManager sm = System.getSecurityManager();if (sm != null) {final int CALLER_FRAME = 3; // 0: Reflection, 1: getProxyClass0 2: Proxy 3: callerfinal Class<?> caller = Reflection.getCallerClass(CALLER_FRAME);final ClassLoader ccl = caller.getClassLoader();checkProxyLoader(ccl, loader);ReflectUtil.checkProxyPackageAccess(ccl, interfaces);}if (interfaces.length > 65535) {throw new IllegalArgumentException("interface limit exceeded");}Class<?> proxyClass = null;/* collect interface names to use as key for proxy class cache */String[] interfaceNames = new String[interfaces.length];// for detecting duplicatesSet<Class<?>> interfaceSet = new HashSet<>();for (int i = 0; i < interfaces.length; i++) {String interfaceName = interfaces[i].getName();Class<?> interfaceClass = null;try {interfaceClass = Class.forName(interfaceName, false, loader);} catch (ClassNotFoundException e) {}if (interfaceClass != interfaces[i]) {throw new IllegalArgumentException(interfaces[i] + " is not visible from class loader");}/** Verify that the Class object actually represents an* interface.*/if (!interfaceClass.isInterface()) {throw new IllegalArgumentException(interfaceClass.getName() + " is not an interface");}/** Verify that this interface is not a duplicate.*/if (interfaceSet.contains(interfaceClass)) {throw new IllegalArgumentException("repeated interface: " + interfaceClass.getName());}interfaceSet.add(interfaceClass);interfaceNames[i] = interfaceName;}List<String> key = Arrays.asList(interfaceNames);/** Find or create the proxy class cache for the class loader.*/Map<List<String>, Object> cache;synchronized (loaderToCache) {cache = loaderToCache.get(loader);if (cache == null) {cache = new HashMap<>();loaderToCache.put(loader, cache);}/** This mapping will remain valid for the duration of this* method, without further synchronization, because the mapping* will only be removed if the class loader becomes unreachable.*/}synchronized (cache) {do {Object value = cache.get(key);if (value instanceof Reference) {proxyClass = (Class<?>) ((Reference) value).get();}if (proxyClass != null) {// proxy class already generated: return itreturn proxyClass;} else if (value == pendingGenerationMarker) {// proxy class being generated: wait for ittry {cache.wait();} catch (InterruptedException e) {}continue;} else {cache.put(key, pendingGenerationMarker);break;}} while (true);}try {String proxyPkg = null;     // package to define proxy class infor (int i = 0; i < interfaces.length; i++) {int flags = interfaces[i].getModifiers();if (!Modifier.isPublic(flags)) {String name = interfaces[i].getName();int n = name.lastIndexOf('.');String pkg = ((n == -1) ? "" : name.substring(0, n + 1));if (proxyPkg == null) {proxyPkg = pkg;} else if (!pkg.equals(proxyPkg)) {throw new IllegalArgumentException("non-public interfaces from different packages");}}}if (proxyPkg == null) {// if no non-public proxy interfaces, use com.sun.proxy packageproxyPkg = ReflectUtil.PROXY_PACKAGE + ".";}{ long num;synchronized (nextUniqueNumberLock) {num = nextUniqueNumber++;}String proxyName = proxyPkg + proxyClassNamePrefix + num; // 标记1.1byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces); // 标记1.2try {proxyClass = defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);// 标记1.3} catch (ClassFormatError e) {throw new IllegalArgumentException(e.toString());}}// add to set of all generated proxy classes, for isProxyClassproxyClasses.put(proxyClass, null);} finally {synchronized (cache) {if (proxyClass != null) {cache.put(key, new WeakReference<Class<?>>(proxyClass));} else {cache.remove(key);}cache.notifyAll();}}return proxyClass;
}

标记1.1完成包名计算。 这里解释了为什么包名是类似com.sun.proxy.$Proxy.NUM 或者PKG.$Proxy.NUM的形式了。

标记1.2完成字节码数组拼接,这个稍后分析。

标记1.3完成字节码数组拼接,最终返回Class对象。

标记1.2的代码如下:

    public static byte[] ProxyGenerator.generateProxyClass(final String name,Class[] interfaces)
{ProxyGenerator gen = new ProxyGenerator(name, interfaces);final byte[] classFile = gen.generateClassFile(); //标记1.2.1if (saveGeneratedFiles) { //标记1.2.2java.security.AccessController.doPrivileged(new java.security.PrivilegedAction() {public Object run() {try {FileOutputStream file =new FileOutputStream(dotToSlash(name) + ".class");file.write(classFile);file.close();return null;} catch (IOException e) {throw new InternalError("I/O exception saving generated file: " + e);}}});}return classFile;
}

在上面代码的标记1.2.1中完成了实际的字节码拼接操作。

在上面代码的标记1.2.2中,使用了这个saveGeneratedFiles变量。而这个变量是这么定义的private final static boolean saveGeneratedFiles = java.security.AccessController.doPrivileged( new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles")).booleanValue();。这个参数可以帮助我们把字节码写到文件中了。

现在,接着看下标记1.2.1处的代码。从下面的代码我们可以看到。该方法先后完成了hashCodeMethod,equalsMethod,toStringMethod的数据准备,所有接口的所有方法。在标记1.2.1.1处完成了带InvocationHandler参数的构造器和静态代码块的数据准备。

为避免正文过长,我把generateConstructorgenerateStaticInitializer挪到附录章节。

private byte[] generateClassFile() {

    /* ============================================================* Step 1: Assemble ProxyMethod objects for all methods to* generate proxy dispatching code for.*//** Record that proxy methods are needed for the hashCode, equals,* and toString methods of java.lang.Object.  This is done before* the methods from the proxy interfaces so that the methods from* java.lang.Object take precedence over duplicate methods in the* proxy interfaces.*/addProxyMethod(hashCodeMethod, Object.class);addProxyMethod(equalsMethod, Object.class);addProxyMethod(toStringMethod, Object.class);/** Now record all of the methods from the proxy interfaces, giving* earlier interfaces precedence over later ones with duplicate* methods.*/for (int i = 0; i < interfaces.length; i++) {Method[] methods = interfaces[i].getMethods();for (int j = 0; j < methods.length; j++) {addProxyMethod(methods[j], interfaces[i]);}}/** For each set of proxy methods with the same signature,* verify that the methods' return types are compatible.*/for (List<ProxyMethod> sigmethods : proxyMethods.values()) {checkReturnTypes(sigmethods);}/* ============================================================* Step 2: Assemble FieldInfo and MethodInfo structs for all of* fields and methods in the class we are generating.*/try {methods.add(generateConstructor()); //标记1.2.1.1for (List<ProxyMethod> sigmethods : proxyMethods.values()) {for (ProxyMethod pm : sigmethods) {// add static field for method's Method objectfields.add(new FieldInfo(pm.methodFieldName,"Ljava/lang/reflect/Method;",ACC_PRIVATE | ACC_STATIC));// generate code for proxy method and add itmethods.add(pm.generateMethod());}}methods.add(generateStaticInitializer()); //标记1.2.1.2} catch (IOException e) {throw new InternalError("unexpected I/O Exception");}if (methods.size() > 65535) {throw new IllegalArgumentException("method limit exceeded");}if (fields.size() > 65535) {throw new IllegalArgumentException("field limit exceeded");}/* ============================================================* Step 3: Write the final class file.*//** Make sure that constant pool indexes are reserved for the* following items before starting to write the final class file.*/cp.getClass(dotToSlash(className));cp.getClass(superclassName);for (int i = 0; i < interfaces.length; i++) {cp.getClass(dotToSlash(interfaces[i].getName()));}/** Disallow new constant pool additions beyond this point, since* we are about to write the final constant pool table.*/cp.setReadOnly();ByteArrayOutputStream bout = new ByteArrayOutputStream();DataOutputStream dout = new DataOutputStream(bout);try {/** Write all the items of the "ClassFile" structure.* See JVMS section 4.1.*/// u4 magic;dout.writeInt(0xCAFEBABE);// u2 minor_version;dout.writeShort(CLASSFILE_MINOR_VERSION);// u2 major_version;dout.writeShort(CLASSFILE_MAJOR_VERSION);cp.write(dout);             // (write constant pool)// u2 access_flags;dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);// u2 this_class;dout.writeShort(cp.getClass(dotToSlash(className)));// u2 super_class;dout.writeShort(cp.getClass(superclassName));// u2 interfaces_count;dout.writeShort(interfaces.length);// u2 interfaces[interfaces_count];for (int i = 0; i < interfaces.length; i++) {dout.writeShort(cp.getClass(dotToSlash(interfaces[i].getName())));}// u2 fields_count;dout.writeShort(fields.size());// field_info fields[fields_count];for (FieldInfo f : fields) {f.write(dout);}// u2 methods_count;dout.writeShort(methods.size());// method_info methods[methods_count];for (MethodInfo m : methods) {m.write(dout);}// u2 attributes_count;dout.writeShort(0); // (no ClassFile attributes for proxy classes)} catch (IOException e) {throw new InternalError("unexpected I/O Exception");}return bout.toByteArray();
}

我们最后使用反编译工具JAD-UI看下生成的代理对象字节码。完整的代码见附录,这里关注下里面的eat()方法。该方法内部的this.h属性就是InvocationHandler的实现类。然后调到invoke方法,完成了最终的执行。

public final void eat(){
try {this.h.invoke(this, m3, null); return;
} catch (Error localError) {throw localError;
} catch (Throwable localThrowable) {throw new UndeclaredThrowableException(localThrowable);
}
}

总结

  1. 最后,我们再回头看看本篇提的问题。这3个参数分别解决了如下几个问题:

    1. ClassLoader loader 解决了使用什么classloader来加载这个代理类
    2. Class<?>[] interfaces 首先约束了JDK动态代理机制是基于接口实现的,它要求我们被代理的类必须实现相应的接口;其次JDK动态代理机制会帮我们完成接口方法的代理方法的实现,并通过硬编码把代理职责委托给了InvocationHandler h 这个参数。
    3. InvocationHandler h 完成了实际的代理职责。InvocationHandler.invoke(Object proxy, Method method, Object[] args)的3个参数:
      1. proxy是生成的代理实例,里面不包含target对象实例。所以,我们一般在实现InvocationHandler接口时,会通过构造方法传入target对象。
      2. method和args 分别对应了 target对象的方法和参数
      3. InvocationHandler.invoke内部再利用反射完成target对象的方法执行。
  2. 在源码面前,一切毫无遁形。
  3. 知其然,尽量要知其所以然。

附录

  1. ProxyGenerator源码

2.构造器数据准备

 private MethodInfo generateConstructor() throws IOException {MethodInfo minfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V",ACC_PUBLIC);DataOutputStream out = new DataOutputStream(minfo.code);code_aload(0, out);code_aload(1, out);out.writeByte(opc_invokespecial);out.writeShort(cp.getMethodRef(superclassName,"<init>", "(Ljava/lang/reflect/InvocationHandler;)V"));out.writeByte(opc_return);minfo.maxStack = 10;minfo.maxLocals = 2;minfo.declaredExceptions = new short[0];return minfo;
}

3.静态块数据准备

private MethodInfo generateStaticInitializer() throws IOException {MethodInfo minfo = new MethodInfo("<clinit>", "()V", ACC_STATIC);int localSlot0 = 1;short pc, tryBegin = 0, tryEnd;DataOutputStream out = new DataOutputStream(minfo.code);for (List<ProxyMethod> sigmethods : proxyMethods.values()) {for (ProxyMethod pm : sigmethods) {pm.codeFieldInitialization(out);}}out.writeByte(opc_return);tryEnd = pc = (short) minfo.code.size();minfo.exceptionTable.add(new ExceptionTableEntry(tryBegin, tryEnd, pc,cp.getClass("java/lang/NoSuchMethodException")));code_astore(localSlot0, out);out.writeByte(opc_new);out.writeShort(cp.getClass("java/lang/NoSuchMethodError"));out.writeByte(opc_dup);code_aload(localSlot0, out);out.writeByte(opc_invokevirtual);out.writeShort(cp.getMethodRef("java/lang/Throwable", "getMessage", "()Ljava/lang/String;"));out.writeByte(opc_invokespecial);out.writeShort(cp.getMethodRef("java/lang/NoSuchMethodError", "<init>", "(Ljava/lang/String;)V"));out.writeByte(opc_athrow);pc = (short) minfo.code.size();minfo.exceptionTable.add(new ExceptionTableEntry(tryBegin, tryEnd, pc,cp.getClass("java/lang/ClassNotFoundException")));code_astore(localSlot0, out);out.writeByte(opc_new);out.writeShort(cp.getClass("java/lang/NoClassDefFoundError"));out.writeByte(opc_dup);code_aload(localSlot0, out);out.writeByte(opc_invokevirtual);out.writeShort(cp.getMethodRef("java/lang/Throwable", "getMessage", "()Ljava/lang/String;"));out.writeByte(opc_invokespecial);out.writeShort(cp.getMethodRef("java/lang/NoClassDefFoundError","<init>", "(Ljava/lang/String;)V"));out.writeByte(opc_athrow);if (minfo.code.size() > 65535) {throw new IllegalArgumentException("code size limit exceeded");}minfo.maxStack = 10;minfo.maxLocals = (short) (localSlot0 + 1);minfo.declaredExceptions = new short[0];return minfo;
}

4.代理类反编译后的源码

package com.vavi.proxy.dynaimc.jdk;import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;import com.vavi.proxy.Sleepable;public final class MyProxy extends Proxy implements Sleepable {
private static Method m1;
private static Method m3;
private static Method m0;
private static Method m4;
private static Method m2;public MyProxy(){
super(paramInvocationHandler);
}public final boolean equals(){
try {return ((Boolean) this.h.invoke(this, m1,new Object[] { paramObject })).booleanValue();
} catch (Error localError) {throw localError;
} catch (Throwable localThrowable) {throw new UndeclaredThrowableException(localThrowable);
}
}@Override
public final void eat(){
try {this.h.invoke(this, m3, null);return;
} catch (Error localError) {throw localError;
} catch (Throwable localThrowable) {throw new UndeclaredThrowableException(localThrowable);
}
}@Override
public final int hashCode(){
try {return ((Integer) this.h.invoke(this, m0, null)).intValue();
} catch (Error localError) {throw localError;
} catch (Throwable localThrowable) {throw new UndeclaredThrowableException(localThrowable);
}
}@Override
public final void sleep(){
try {this.h.invoke(this, m4, null);return;
} catch (Error localError) {throw localError;
} catch (Throwable localThrowable) {throw new UndeclaredThrowableException(localThrowable);
}
}@Override
public final String toString(){
try {return ((String) this.h.invoke(this, m2, null));
} catch (Error localError) {throw localError;
} catch (Throwable localThrowable) {throw new UndeclaredThrowableException(localThrowable);
}
}static {
try {m1 = Class.forName("java.lang.Object").getMethod("equals",new Class[] { Class.forName("java.lang.Object") });m3 = Class.forName("com.vavi.proxy.Sleepable").getMethod("eat",new Class[0]);m0 = Class.forName("java.lang.Object").getMethod("hashCode",new Class[0]);m4 = Class.forName("com.vavi.proxy.Sleepable").getMethod("sleep",new Class[0]);m2 = Class.forName("java.lang.Object").getMethod("toString",new Class[0]);return;
} catch (NoSuchMethodException localNoSuchMethodException) {throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
} catch (ClassNotFoundException localClassNotFoundException) {throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
}

转载于:https://my.oschina.net/geecoodeer/blog/204138

JDK动态代理学习笔记相关推荐

  1. java jdk动态代理学习记录

    转载自: https://www.jianshu.com/p/3616c70cb37b JDK自带的动态代理主要是指,实现了InvocationHandler接口的类,会继承一个invoke方法,通过 ...

  2. Java_JDK动态代理学习笔记

    昨天被问了个问题,问题的大意是这样的:为什么 Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, Invoc ...

  3. 【Java高级程序设计学习笔记】深入理解jdk动态代理

    java的设计模式中有一项设计模式叫做代理模式,所谓代理模式,就是通过代理方来操作目标对象,而不是自己直接调用.代理又分为静态代理和动态代理,静态代理就是针对每个被代理对象写一个代理类,操作不够优雅: ...

  4. Spring原理学习(七)JDK动态代理与CGLIB代理底层实现

    AOP 底层实现方式之一是代理,由代理结合通知和目标,提供增强功能. 除此以外,aspectj 提供了两种另外的 AOP 底层实现: 第一种是通过 ajc 编译器在编译 class 类文件时,就把通知 ...

  5. 代理模式及JDK动态代理(InvocationHandler)的简单实现与分析

    在慕课网上学习了讲解代理模式的一个课程--<模式的秘密--代理模式>,感叹于David老师屌炸天的PPT,同时,老师一步一步模仿JDK源码去写code,教我们去简单实现JDK中的动态代理, ...

  6. cglib动态代理和jdk动态代理的区别与应用

    1,引入 如果从一个Controller调用Service的非事务方法a,然后在a里调用事务方法b,b事务生效吗? public void update() {updateActual();int a ...

  7. 静态代理和JDK动态代理

    (开发环境是MyEclipse) 静态代理示例 HellowStaticProxy代理, HellowObject被代理 HellowObject和HellowStaticProxy实现iHellow ...

  8. 输出cglib以及jdk动态代理产生的class文件

    好奇心重的小伙伴有一种知其然,亦欲知其所以然的特性,我们在spring事务应用中会接触到aop技术,而aop背后隐藏的恰恰是以jdk以及cglib为基础的动态代理技术,博主不才,将自己的学习历程记录于 ...

  9. 【动态代理】从源码实现角度剖析JDK动态代理

    相比于静态代理,动态代理避免了开发人员编写各个繁锁的静态代理类,只需简单地指定一组接口及目标类对象就能动态的获得代理对象.动态代理类的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代 ...

最新文章

  1. 20行python代码的入门级小游戏-200行Python代码实现的2048小游戏
  2. qa dataset
  3. Python利用turtle绘制五角星
  4. 【文末福利】如何用精密算法解决未婚妻问题?
  5. 大学生在校期间可以考哪些证书?
  6. 港府多措施推广使用电动车 放宽条件吸引车主换车
  7. java-redis初探
  8. Nodejs解压版安装
  9. FPGA 按键控制数码管
  10. SSL证书不受信任怎么办?重点关注这4点
  11. 双绞线的规范和制作经验谈
  12. python在地图上标注点_只要两步,用Python将地址标记在地图上!
  13. Java8 stream新定义运算
  14. Windows10 开机密码破解
  15. 如何实现 AppStore App 的自动下载
  16. Element 根据勾选导出Excel表格数据
  17. 培训2022年6月22日
  18. [Error] invalid operands of types ‘float‘ and ‘float‘ to binary ‘operator%‘
  19. 96-Java的打印流、打印流重定向、补充知识:Properties、commons-io框架
  20. 如何构建关系型数据库

热门文章

  1. MODBUS-RTU通讯协议简介
  2. 数智随行 | 探想未来工厂数字化,强化智能设备管理
  3. opensuse zypper源
  4. jspm校园健康管理系统毕业设计(附源码、运行环境)
  5. Linux使用花生壳进行内网穿透
  6. 新手入门阿里云服务器操作指南(图文教程)
  7. 在西安软件业,寻找朋友(群号:205653636 群名:西安软件主宰者)
  8. HTML版图像精灵制作工具
  9. GD库 图片水印+文字水印+缩率图+圆形图
  10. portainer(2):在raspberryPi 3b+上面安装docker 和 portainer 的 agent